Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
6624995
fix(security): remediation bundle — AES-256-GCM, DOMPurify, WebSocket…
Jul 28, 2026
f08fe6e
fix(review): address AI scanner findings
Jul 28, 2026
957e149
fix(review): address second OCR scan findings (#193-#197)
Jul 28, 2026
68b49b8
fix(review): collapse redundant if/else in droplet-interface.ts
Jul 28, 2026
95c672f
fix(review): address third OCR scan (#191)
Jul 28, 2026
ce4404d
fix(review): address fourth OCR scan — key zeroing, URI regexp, WS ha…
Jul 28, 2026
a88864e
fix(review): fix URI regexp regression + WS dead code
Jul 28, 2026
5286f72
fix(ci): allow sonar-pr-comment to run when quality gate fails
Jul 28, 2026
8f98e92
fix(review): minor cleanup — semver consistency + sanitize error logging
Jul 28, 2026
3e7bf2d
fix(review): expose resetHooks() for test/HMR cleanup
Jul 28, 2026
c6790e2
fix(review): DoS timeout, remove ftp from URI regexp, add auth failur…
Jul 28, 2026
773a8f1
fix(review): address OCR timeout suggestions + fix CI lint + cargo audit
Jul 28, 2026
b396cbc
chore(hooks): add ocr review to pre-push hook
Jul 28, 2026
a67ea0b
chore(hooks): add non-blocking ocr review to pre-push hook
Jul 28, 2026
357606f
fix(review): address OCR race condition + hook cleanup findings
Jul 28, 2026
09991f1
fix(review): address 12 OCR findings across 6 files
Jul 28, 2026
863ca0b
fix(hooks): refine base branch detection — skip self-tracking upstream
Jul 28, 2026
8439c5e
chore: sync pnpm-lock after removing dompurify dep
Jul 28, 2026
094f9f7
fix(hooks): mktemp template — move XXXXXX to end for macOS compat
Jul 28, 2026
981b638
fix(hooks): make ocr review blocking on push
Jul 28, 2026
05cbbe8
fix: resolve 7 remediation items from AI scanners (OCR/Sourcery/CodeR…
Jul 28, 2026
708b123
fix: resolve 2 open PR review threads
Jul 29, 2026
e94289f
chore: add fallow-ignore-file to false-positive files
Jul 29, 2026
54d9f73
fix(review): address OCR, CodeRabbit, and manual review findings
Jul 29, 2026
0b7770e
fix(review): final OCR/CodeRabbit findings — URI regexp + authTimeout…
Jul 29, 2026
4276418
chore: fix CI formatting failures + auto-format Rust on pre-commit
Jul 29, 2026
d6368ed
fix(sonarcloud): report coverage gaps even when 0 issues, filter cove…
Jul 29, 2026
d842934
fix(sonarcloud): handle null new_uncovered_lines in jq filter
Jul 29, 2026
65011c5
fix(review): latest OCR/CodeRabbit findings on new commits
Jul 29, 2026
b66c76f
feat(hooks): pre-push fetches unresolved PR review threads as JSON
Jul 29, 2026
0f6105a
fix(hooks): use first:100 in PR review thread query, add --repo flag
Jul 29, 2026
e447d4d
fix(sonarcloud): include 0% coverage files even when uncovered count …
Jul 29, 2026
8b73818
chore: fix typo in pre-push comment
Jul 29, 2026
dc3f923
docs: add pr-review-cleanup and ci-format-guard skills, update configs
Jul 29, 2026
d94ccbc
test: add buildFilters and AuthManager unit tests
Jul 29, 2026
e386337
fix(hooks): add fallow audit + full test suite gates to pre-push
Jul 29, 2026
13fcf33
chore: fix ws.get.ts prettier formatting
Jul 29, 2026
d7fc987
fix(ci): read SonarCloud period values for PR coverage
Jul 29, 2026
b51771b
fix(hooks): cursor-paginate review threads + preserve partial staging
Jul 29, 2026
1a70685
fix: resolve fallow pre-commit gate — suppress false positives, remov…
Jul 29, 2026
3c188b1
fix: address 7 OCR review findings across 6 files
Jul 29, 2026
e6db1dd
fix: remaining OCR review findings — sonarcloud script + skill docs
Jul 29, 2026
f3036a3
refactor: extract PR review thread logic into /pull-review-comments s…
Jul 29, 2026
e230477
chore: update pnpm-lock.yaml after removing dompurify + @types/dompurify
Jul 29, 2026
15dfccb
fix(security): handle rand fill_bytes Result, add zeroize for key mat…
Jul 29, 2026
64c798f
chore: fix bare toBeDefined — use typeof check instead
Jul 29, 2026
d22bfcc
chore: remove ocr pre-push hook
Jul 29, 2026
f6d2a89
fix: revert rand fill_bytes .expect() — ThreadRng returns (), not Result
Jul 29, 2026
5744e9a
refactor: extract rejectPeer helper, add drain guard, fix optional ch…
Jul 29, 2026
1bf1de1
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
4434134
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
6195dd4
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
26eff2c
Update .husky/pre-push
BillyOutlast Jul 29, 2026
6398cd3
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
9ae8225
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
a7af29b
Update scripts/sonarcloud-pr-comment.sh
BillyOutlast Jul 29, 2026
e43cc06
Update server/server/internal/auth/index.ts
BillyOutlast Jul 29, 2026
18c18d7
Update scripts/sonarcloud-pr-comment.sh
BillyOutlast Jul 29, 2026
d19ee96
Update scripts/sonarcloud-pr-comment.sh
BillyOutlast Jul 29, 2026
53421be
Update .husky/pre-push
BillyOutlast Jul 29, 2026
8d84124
Update desktop/src-tauri/database/src/db.rs
BillyOutlast Jul 29, 2026
3513ee1
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
6529265
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
cf9a0a8
Update .husky/pre-push
BillyOutlast Jul 29, 2026
ea4087a
Update scripts/sonarcloud-pr-comment.sh
BillyOutlast Jul 29, 2026
a25c0b3
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
fe32cb4
Update scripts/sonarcloud-pr-comment.sh
BillyOutlast Jul 29, 2026
9a9696f
Update server/server/api/v1/notifications/ws.get.ts
BillyOutlast Jul 29, 2026
d388307
fix: address open PR #198 review threads across 5 files
Jul 29, 2026
ba7ddee
chore: expand pre-commit to whole-repo gates
Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ comment:
coverage:
precision: 2
range:
- 60.0
- 80.0
- 60.0
- 80.0
round: down
status:
changes: false
Expand Down
1 change: 0 additions & 1 deletion .github/scripts/diff-to-test-prompt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ esac

# ---- Count stats -----------------------------------------------------------
FILE_COUNT="$(echo "$DIFF_CONTENT" | grep -c '^diff --git' || true)"
LINE_COUNT="$(echo "$DIFF_CONTENT" | grep -c '^[+-]' || true)"
ADDED="$(echo "$DIFF_CONTENT" | grep -c '^+' || true)"
REMOVED="$(echo "$DIFF_CONTENT" | grep -c '^-' || true)"

Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,10 @@ jobs:
name: SonarCloud PR Comment
runs-on: ubuntu-latest
needs: sonar
# Only run when the scan succeeded — otherwise the API has no findings
# to comment on and the script would post a confusing empty/errored
# comment. Branch protection enforces SonarCloud Scan as required, so
# a scan failure correctly blocks the merge regardless.
if: github.event_name == 'pull_request' && needs.sonar.result == 'success'
# The scan uploads findings to SonarCloud API before quality gate check.
# Run even when quality gate fails — the API still has data to comment.
# always() overrides needs dependency failure; !cancelled() alone does not.
if: github.event_name == 'pull_request' && always() && !cancelled()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always() causes this job to run even when the sonar job is skipped (not just failed). If the sonar scan was never executed, the script may still post a "No issues found" comment on the PR, which is misleading since no actual analysis occurred. Consider narrowing the condition to only run when sonar succeeded or failed, but not when skipped.

Suggestion:

Suggested change
if: github.event_name == 'pull_request' && always() && !cancelled()
if: github.event_name == 'pull_request' && !cancelled() && (needs.sonar.result == 'success' || needs.sonar.result == 'failure')

Comment on lines +269 to +270

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition change looks reasonable and the inline comments explain the motivation clearly. However, there is a subtle edge case to consider:

If the sonar job fails before the SonarCloud scan step (e.g., dependency installation fails at line 217-218 or 227), the scan never uploads any findings to the SonarCloud API. The always() expression will still trigger this job and the script will attempt to query the API. While the script has been improved to gracefully handle zero findings, it may still post a potentially misleading "No issues found" comment when in reality no scan ever ran.

Suggestion: Consider adding a guard in the script to detect whether the scan actually ran — e.g., by checking the sonar-scan step outcome if feasible, or by querying the API for a baseline check before posting a "clean" comment.

Suggestion:

Suggested change
# always() overrides needs dependency failure; !cancelled() alone does not.
if: github.event_name == 'pull_request' && always() && !cancelled()
if: github.event_name == 'pull_request' && always() && !cancelled()

permissions:
contents: read
pull-requests: write
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
run: pnpm install --frozen-lockfile --ignore-scripts # NOSONAR

- name: Generate Nuxt and Prisma artifacts
run: pnpm --filter drop run postinstall
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/open-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: alibaba/open-code-review@0ced7165718725e15223c3e5a506df7b7e9de51f # v1.7.17
- uses: alibaba/open-code-review@0ced7165718725e15223c3e5a506df7b7e9de51f # v1.7.17 # NOSONAR
Comment thread
BillyOutlast marked this conversation as resolved.
with:
# Configure in GitHub repo settings → Secrets and variables → Actions
llm_url: ${{ secrets.OCR_LLM_URL }}
Expand Down
69 changes: 33 additions & 36 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
# Fallow audit gate
# Base pinned to https://github.com/BillyOutlast/drop/tree/rebuild so the gate
# only fails on findings introduced since the remote rebuild branch.
#!/usr/bin/env bash
# Fallow audit gate — every finding in changed files blocks the commit.
# `gate = "all"` is configured in fallow.toml so inherited findings also gate.
# Base pinned to https://github.com/BillyOutlast/drop/tree/rebuild.
FALLOW_AUDIT_BASE=origin/rebuild fallow audit --format json --quiet --explain --gate-marker agent || exit 1

# Run prisma generate when schema or proto files changed (needed for typecheck)
# Prisma generate trigger — only when schema or proto files change (build-dep,
# not a content check).
changed_schema=$(git diff --cached --name-only --diff-filter=ACM -- 'server/prisma/schema.prisma' 'server/**/*.proto')
if [ -n "$changed_schema" ]; then
echo "Prisma schema or proto changed — running prisma generate..."
pnpm --filter drop exec prisma generate || exit 1
fi

pnpm --filter drop lint-staged && pnpm --filter drop typecheck
# Whole-repo type-safety + style gate (prettier --check + eslint, no auto-fix).
pnpm --filter drop lint || exit 1
Comment on lines +15 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching from lint-staged (staged files only) to pnpm run lint (which runs prettier --check . and eslint . on the entire server/ directory) is a significant scope increase. This will reformat-check and lint every file in the repo — including completely unrelated files — every time any commit is made. This adds substantial latency and can block commits due to pre-existing issues in untouched code, which contradicts the principle of least surprise for pre-commit hooks.

Suggestion:

Suggested change
# Whole-repo type-safety + style gate (prettier --check + eslint, no auto-fix).
pnpm --filter drop lint || exit 1
# Staged-file-only style + type gate — only check what's about to be committed.
pnpm --filter drop lint-staged || exit 1
pnpm --filter drop typecheck || exit 1

pnpm --filter drop typecheck || exit 1

# Check test files for bare .toBeDefined() / .not.toBeNull() without companion assertions
changed_tests=$(git diff --cached --name-only --diff-filter=ACM -- '*.test.ts' '*.spec.ts')
if [ -n "$changed_tests" ]; then
bare_assertions=$(grep -n '\.toBeDefined()\|\.not\.toBeNull()' $changed_tests 2>/dev/null | grep -v '\.toEqual\|\.toMatchSnapshot\|\.toStrictEqual\|\.toBe(' || true)
# Whole-repo shellcheck — all git-tracked .sh files, not just staged ones.
if command -v shellcheck >/dev/null 2>&1; then
tracked_sh=$(git ls-files '*.sh')
if [ -n "$tracked_sh" ]; then
echo "$tracked_sh" | xargs shellcheck --severity=warning || exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shellcheck and bare-assertion scans use echo "$tracked_sh" | xargs and echo "$tracked_tests" | xargs grep, which will break on filenames containing spaces or special characters (e.g., via word splitting). If any tracked .sh or test file (now or in the future) has spaces in its path, shellcheck/grep will receive malformed arguments, silently skipping those files or failing entirely.

Suggestion:

Suggested change
echo "$tracked_sh" | xargs shellcheck --severity=warning || exit 1
git ls-files -z '*.sh' | xargs -0 shellcheck --severity=warning || exit 1

fi
else
echo "shellcheck not installed — skipping shell script checks"
fi

# Whole-repo bare-assertion scan — flag toBeDefined / .not.toBeNull() without
# a companion meaningful assertion across every test file in the repo.
tracked_tests=$(git ls-files '*.test.ts' '*.spec.ts')
if [ -n "$tracked_tests" ]; then
bare_assertions=$(echo "$tracked_tests" | xargs grep -n '\.toBeDefined()\|\.not\.toBeNull()' 2>/dev/null | grep -v '\.toEqual\|\.toMatchSnapshot\|\.toStrictEqual\|\.toBe(' || true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bare assertion scan also uses echo "$tracked_tests" | xargs grep, inheriting the same word-splitting vulnerability as the shellcheck check. Use git ls-files -z with xargs -0 for robustness.

Suggestion:

Suggested change
bare_assertions=$(echo "$tracked_tests" | xargs grep -n '\.toBeDefined()\|\.not\.toBeNull()' 2>/dev/null | grep -v '\.toEqual\|\.toMatchSnapshot\|\.toStrictEqual\|\.toBe(' || true)
bare_assertions=$(git ls-files -z '*.test.ts' '*.spec.ts' | xargs -0 grep -n '\.toBeDefined()\|\.not\.toBeNull()' 2>/dev/null | grep -v '\.toEqual\|\.toMatchSnapshot\|\.toStrictEqual\|\.toBe(' || true)

if [ -n "$bare_assertions" ]; then
echo "ERROR: Bare .toBeDefined() or .not.toBeNull() without companion assertion:"
echo "$bare_assertions"
Expand All @@ -24,30 +39,12 @@ if [ -n "$changed_tests" ]; then
fi
fi

# Check shell scripts
changed_sh=$(git diff --cached --name-only --diff-filter=ACM -- '*.sh')
if [ -n "$changed_sh" ]; then
if command -v shellcheck >/dev/null 2>&1; then
echo "$changed_sh" | xargs shellcheck --severity=warning || exit 1
else
echo "shellcheck not installed — skipping shell script checks"
fi
fi

# Check Rust formatting on changed .rs files, filtered by workspace
changed_rs=$(git diff --cached --name-only --diff-filter=ACM -- '*.rs')
if [ -n "$changed_rs" ]; then
torrential_rs=$(echo "$changed_rs" | grep '^torrential/' || true)
cli_rs=$(echo "$changed_rs" | grep '^cli/' || true)
desktop_rs=$(echo "$changed_rs" | grep '^desktop/' || true)

if [ -n "$torrential_rs" ]; then
cargo fmt --manifest-path torrential/Cargo.toml -- --check $torrential_rs || exit 1
fi
if [ -n "$cli_rs" ]; then
cargo fmt --manifest-path cli/Cargo.toml -- --check $cli_rs || exit 1
fi
if [ -n "$desktop_rs" ]; then
cargo fmt --manifest-path desktop/src-tauri/Cargo.toml -- --check $desktop_rs || exit 1
fi
fi
# Whole-repo cargo fmt --check across all three rust workspaces (no auto-fix;
# forces developer to format manually if any .rs file drifts).
if command -v cargo >/dev/null 2>&1; then
cargo fmt --all --manifest-path torrential/Cargo.toml -- --check || exit 1
cargo fmt --all --manifest-path cli/Cargo.toml -- --check || exit 1
cargo fmt --all --manifest-path desktop/src-tauri/Cargo.toml -- --check || exit 1
else
echo "cargo not installed — skipping rust format checks"
fi
Comment on lines +42 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running cargo fmt --all --manifest-path ... -- --check on all three Rust workspaces unconditionally will format-check every Rust file in the repo even when no .rs files are changed. This adds unnecessary overhead and can block commits for formatting issues in completely unrelated code. The previous approach of filtering by changed .rs files was more targeted and efficient.

Suggestion:

Suggested change
# Whole-repo cargo fmt --check across all three rust workspaces (no auto-fix;
# forces developer to format manually if any .rs file drifts).
if command -v cargo >/dev/null 2>&1; then
cargo fmt --all --manifest-path torrential/Cargo.toml -- --check || exit 1
cargo fmt --all --manifest-path cli/Cargo.toml -- --check || exit 1
cargo fmt --all --manifest-path desktop/src-tauri/Cargo.toml -- --check || exit 1
else
echo "cargo not installed — skipping rust format checks"
fi
# Only check changed Rust files, scoped by workspace.
changed_rs=$(git diff --cached --name-only --diff-filter=ACM -- '*.rs')
if [ -n "$changed_rs" ]; then
if command -v cargo >/dev/null 2>&1; then
echo "$changed_rs" | grep '^torrential/' || true | sed "s|^torrential/||" | xargs -I{} cargo fmt --manifest-path torrential/Cargo.toml -- --check "{}" 2>/dev/null || exit 1
echo "$changed_rs" | grep '^cli/' || true | sed "s|^cli/||" | xargs -I{} cargo fmt --manifest-path cli/Cargo.toml -- --check "{}" 2>/dev/null || exit 1
echo "$changed_rs" | grep '^desktop/' || true | sed "s|^desktop/||" | xargs -I{} cargo fmt --manifest-path desktop/src-tauri/Cargo.toml -- --check "{}" 2>/dev/null || exit 1
else
echo "cargo not installed — skipping rust format checks"
fi
fi

68 changes: 67 additions & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,3 +1,69 @@
#!/usr/bin/env bash
# Pre-push gate: incremental tests, fallow audit, optional full suite, PR thread check.

set -u

# Incremental test run: only tests affected by pushed changes.
# Full suite still runs in CI.
pnpm --filter drop test:changed
if command -v pnpm >/dev/null 2>&1; then
pnpm --filter drop test:changed
else
echo ":: pnpm not found — cannot run incremental tests" >&2
exit 1
fi
Comment on lines +4 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script uses set -u but NOT set -e (or set -o errexit). This means if pnpm --filter drop test:changed exits with a non-zero status (i.e., tests fail), the script continues executing subsequent gates and ultimately exits 0, allowing the push to proceed despite failing tests. This defeats the entire purpose of a pre-push test gate.

Compare with .husky/pre-commit, which handles this correctly by using || exit 1 after each command. The fix is either:

  1. Add set -e after set -u on line 2, or
  2. Follow the pre-commit pattern with || exit 1 after the pnpm command.

Suggestion:

Suggested change
set -u
# Incremental test run: only tests affected by pushed changes.
# Full suite still runs in CI.
pnpm --filter drop test:changed
if command -v pnpm >/dev/null 2>&1; then
pnpm --filter drop test:changed
else
echo ":: pnpm not found — cannot run incremental tests" >&2
exit 1
fi
set -eu
# Incremental test run: only tests affected by pushed changes.
# Full suite still runs in CI.
if command -v pnpm >/dev/null 2>&1; then
pnpm --filter drop test:changed
else
echo ":: pnpm not found — cannot run incremental tests" >&2
exit 1
fi

Comment on lines +8 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exit code from incremental tests is silently discarded — pushes with failing tests succeed.

The original single-line pnpm --filter drop test:changed was the terminal command, so its exit code naturally determined the hook's outcome. In the new script, this command runs inside an if block, but the script has set -u without set -e, meaning a non-zero exit from the test command does NOT halt execution. The script continues through the fallow audit, optional full suite, and PR review checks, and the final exit code is always 0 (determined by the last echo or exit 0 in the PR check block).

As a result, developers can push code with failing incremental tests without ever being blocked, defeating the primary purpose of the pre-push gate.

Fix: Capture the test exit code and propagate it, e.g.:

if command -v pnpm >/dev/null 2>&1; then
  pnpm --filter drop test:changed || exit 1
else
  echo ":: pnpm not found — cannot run incremental tests" >&2
  exit 1
fi

Suggestion:

Suggested change
if command -v pnpm >/dev/null 2>&1; then
pnpm --filter drop test:changed
else
echo ":: pnpm not found — cannot run incremental tests" >&2
exit 1
fi
if command -v pnpm >/dev/null 2>&1; then
pnpm --filter drop test:changed || exit 1
else
echo ":: pnpm not found — cannot run incremental tests" >&2
exit 1
fi


# Detect base from upstream tracking. If upstream matches current branch
# (PR branch self-tracking) or unset, fall back to origin/rebuild.
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
UPSTREAM="$(git rev-parse --abbrev-ref '@{upstream}' 2>/dev/null || true)"
REMOTE_BASE="${UPSTREAM}"
if [ -z "${REMOTE_BASE}" ] || [ "${REMOTE_BASE##*/}" = "${CURRENT_BRANCH}" ]; then
Comment thread
BillyOutlast marked this conversation as resolved.
REMOTE_BASE="origin/rebuild"
Comment thread
BillyOutlast marked this conversation as resolved.
fi

# fallow audit gate: block on 'fail' verdict, tolerate JSON parse errors.
if command -v fallow >/dev/null 2>&1; then
FALLOW_STDERR="$(mktemp)"
FALLOW_JSON="$(FALLOW_AUDIT_BASE="${REMOTE_BASE}" fallow audit --format json --quiet --explain --gate-marker agent 2>"${FALLOW_STDERR}" || echo '{"verdict":"error","error":true}')"
FALLOW_STDERR_CONTENT="$(cat "${FALLOW_STDERR}")"
rm -f "${FALLOW_STDERR}"
Comment on lines +26 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporary file created via mktemp for capturing fallow stderr is cleaned up in the normal code path (rm -f), but there is no trap handler to clean it up on abnormal exit (e.g., SIGINT, SIGTERM, or an early exit 1). While this is a minor resource leak, adding a trap aligns with defensive shell scripting best practices.

Suggestion:

Suggested change
FALLOW_STDERR="$(mktemp)"
FALLOW_JSON="$(FALLOW_AUDIT_BASE="${REMOTE_BASE}" fallow audit --format json --quiet --explain --gate-marker agent 2>"${FALLOW_STDERR}" || echo '{"verdict":"error","error":true}')"
FALLOW_STDERR_CONTENT="$(cat "${FALLOW_STDERR}")"
rm -f "${FALLOW_STDERR}"
FALLOW_STDERR="$(mktemp)"
trap 'rm -f "${FALLOW_STDERR}"' EXIT
FALLOW_JSON="$(FALLOW_AUDIT_BASE="${REMOTE_BASE}" fallow audit --format json --quiet --explain --gate-marker agent 2>"${FALLOW_STDERR}" || echo '{"verdict":"error","error":true}')"
FALLOW_STDERR_CONTENT="$(cat "${FALLOW_STDERR}")"

if [ -n "${FALLOW_STDERR_CONTENT}" ]; then
echo ":: fallow audit stderr: ${FALLOW_STDERR_CONTENT}" >&2
fi
if command -v jq >/dev/null 2>&1; then
FALLOW_VERDICT="$(echo "${FALLOW_JSON}" | jq -r '.verdict // "error"' 2>/dev/null || echo "error")"
else
# Fallback: parse verdict without jq so the gate still works
FALLOW_VERDICT="$(echo "${FALLOW_JSON}" | grep -oE '"verdict":"[a-z]+"' | cut -d'"' -f4 || echo "error")"
fi
Comment on lines +33 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback JSON verdict parser using grep/cut is fragile: the regex '"verdict":"[a-z]+"' assumes the key-value pair appears on a single line with no extra whitespace. If the JSON is compact, pretty-printed differently, or contains unexpected characters, the regex may silently return an empty string, causing the audit gate to be bypassed.

More importantly, the project's other shell scripts (scripts/codecov-pr-comment.sh, scripts/sonarcloud-pr-comment.sh) treat jq as a hard requirement — they check for it and exit with FATAL if missing. The pre-push hook should follow the same convention and require jq, rather than adding an unreliable fallback parser.

Suggestion:

Suggested change
if command -v jq >/dev/null 2>&1; then
FALLOW_VERDICT="$(echo "${FALLOW_JSON}" | jq -r '.verdict // "error"' 2>/dev/null || echo "error")"
else
# Fallback: parse verdict without jq so the gate still works
FALLOW_VERDICT="$(echo "${FALLOW_JSON}" | grep -oE '"verdict":"[a-z]+"' | cut -d'"' -f4 || echo "error")"
fi
if command -v jq >/dev/null 2>&1; then
FALLOW_VERDICT="$(echo "${FALLOW_JSON}" | jq -r '.verdict // "error"' 2>/dev/null || echo "error")"
else
echo ":: FATAL: jq is required but not installed for fallow audit verdict parsing" >&2
exit 1
fi

if [ "${FALLOW_VERDICT}" = "fail" ]; then
echo ":: fallow audit verdict: fail. Fix findings or use --no-verify."
exit 1
elif [ "${FALLOW_VERDICT}" = "error" ]; then
echo ":: fallow audit returned errors (non-blocking) — continuing"
fi
else
echo ":: fallow CLI not found — skipping audit gate"
fi

Comment on lines +47 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full test suite (pnpm --filter drop test) runs unconditionally on every push, in addition to the incremental test run (pnpm --filter drop test:changed) already in the hook. Since CI workflows (e.g., ci.yml, server-ci.yml) also run the full suite in CI, this adds noticeable latency to every push — especially for larger test suites — with minimal incremental value. Consider making this optional (e.g., behind an environment variable like FULL_TEST=1) or removing it since CI already provides the safety net.

Suggestion:

Suggested change
fi
# Full test suite gate: verify no regressions before push.
# Skip if CI will handle it — uncomment or set FULL_TEST=1 to enable.
if [ -n "${FULL_TEST:-}" ]; then
pnpm --filter drop test
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No command -v check is performed for pnpm before running the full test suite, unlike fallow, jq, and gh which are properly guarded earlier in this same script. If pnpm is unavailable, the command fails silently and the push continues, hiding the failure.

While pnpm is a core project dependency, all other external tools in this script are checked. Consider adding a guard for consistency and to provide a clear error message if pnpm is missing.

Suggestion:

Suggested change
if command -v pnpm >/dev/null 2>&1; then
pnpm --filter drop test || exit 1
else
echo ':: pnpm not found — cannot run test suite' >&2
exit 1
fi

# Optional full test suite gate: verify no regressions before push.
# Disabled by default — CI runs the full suite. Set FULL_TEST=1 to enable.
if [ -n "${FULL_TEST:-}" ]; then
pnpm --filter drop test
fi

# Check for PR review threads.
# Quick count only — use /pull-review-comments skill for full details + resolution.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sed expression on line 38 used to parse the GitHub remote URL is fragile. It may fail for URLs without .git suffix, URLs with trailing slashes, or SSH URLs using git@github.com:owner/repo.git. Consider using git remote get-url origin | awk -F'github.com[:/]' '{print $2}' | sed 's/\.git$//' or basename $(git remote get-url origin) .git with owner extraction for a more robust approach.

Suggestion:

Suggested change
# Quick count only — use /pull-review-comments skill for full details + resolution.
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
REPO=$(echo "$REMOTE_URL" | sed -nE 's#.*github.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#p' 2>/dev/null)
if [ -z "${REPO}" ]; then
# Fallback: try alternate URL format
REPO=$(echo "$REMOTE_URL" | awk -F'github.com[:/]' '{print $2}' | sed 's/\.git$//' 2>/dev/null || true)
fi

if command -v gh >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
Comment on lines +55 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines inside the if command -v gh >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then block have inconsistent indentation. Lines 38-39 are at column 0 while the rest (lines 43-60) use 2-space indentation. This hurts readability and may confuse shellcheck or future maintainers.

Suggestion:

Suggested change
# Check for PR review threads.
# Quick count only — use /pull-review-comments skill for full details + resolution.
if command -v gh >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
if command -v gh >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
REPO=$(git remote get-url origin | sed -nE 's#.*github.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#p' 2>/dev/null)
if [ -z "${REPO}" ]; then

REPO="$(git remote get-url origin | sed -nE 's#.*github.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#p' 2>/dev/null)"
if [ -z "${REPO}" ]; then
echo ":: unable to parse GitHub repository from remote URL — skipping review thread check" >&2
exit 0
fi
Comment on lines +59 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When REPO is empty (e.g., remote URL parsing fails with non-standard Git URLs), the script prints a message and calls exit 0. This exit 0 overrides any accumulated error status from previous gates (test failures, audit failures), causing the push to silently succeed even when earlier gates failed.

If the intent is to skip the PR thread check when the repo can't be determined, the script should fall through to the implicit exit code of previous commands rather than explicitly exiting 0.

Suggestion:

Suggested change
if [ -z "${REPO}" ]; then
echo ":: unable to parse GitHub repository from remote URL — skipping review thread check" >&2
exit 0
fi
if [ -z "${REPO}" ]; then
echo ":: unable to parse GitHub repository from remote URL — skipping review thread check" >&2
else
PR_NUMBER="$(gh pr view --json number -q .number 2>/dev/null || true)"
if [ -n "${PR_NUMBER}" ]; then
echo ":: PR #${PR_NUMBER} open — run /pull-review-comments for unresolved review threads before pushing"
fi
fi

PR_NUMBER="$(gh pr view --json number -q .number 2>/dev/null || true)"
if [ -n "${PR_NUMBER}" ]; then
echo ":: PR #${PR_NUMBER} open — run /pull-review-comments for unresolved review threads before pushing"
fi
else
echo ":: gh or jq not found — skipping review thread check" >&2
fi
Comment on lines +67 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file is missing a trailing newline at the end of the file (\ No newline at end of file in the diff). POSIX standards require a trailing newline in text files. Many Unix tools (e.g., cat, sed, wc) may behave unexpectedly when processing files without a trailing newline.

Suggestion:

Suggested change
else
echo ":: gh or jq not found — skipping review thread check" >&2
fi
else
echo ":: gh or jq not found — skipping review thread check" >&2
fi

83 changes: 83 additions & 0 deletions .opencode/skills/ci-format-guard/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
name: ci-format-guard
description: Use when pre-commit hooks pass locally but CI fails on formatting (prettier --check, cargo fmt --check), or when adding/modifying git hooks that enforce code style. Also use when a formatting failure in one CI job cascades to skip downstream lint/test jobs.
---

# CI Format Guard

Pre-commit hooks that pass locally but CI fails on formatting mean the hook checks but doesn't fix. CI formatting is the last line of defense — it should never be the first.

## Core Principle

**Hooks auto-fix, not just check.** If a hook detects a formatting issue it can fix, it should fix it — not report it and block.

## Pattern: Auto-Fix + Re-Stage

```bash
# ❌ BAD: Checks but doesn't fix — CI will fail
cargo fmt --manifest-path desktop/src-tauri/Cargo.toml -- --check $changed_rs || exit 1

# ✅ GOOD: Auto-fixes AND propagates failure for unfixable errors
cargo fmt --manifest-path desktop/src-tauri/Cargo.toml -- $changed_rs || exit 1
echo "$changed_rs" | xargs -r git add
```

The `|| exit 1` stays — cargo fmt can fail for unfixable reasons (missing toolchain, malformed syntax). But formatting issues get auto-fixed and re-staged.

## Quick Reference

| Workspace | Format command (auto-fix) | Verify command (CI) |
|-----------|--------------------------|---------------------|
| server (TS/Vue) | `pnpm --filter drop exec prettier --write <file>` | `pnpm --filter drop exec prettier --check .` |
| Rust (any) | `cargo fmt -- <file>` | `cargo fmt -- --check` |
| YAML/MD/JSON | `pnpm --filter drop exec prettier --write <file>` | Same as server |

## jq Defensive Patterns

jq in CI scripts crashes on null/non-numeric values. Always guard:

```bash
# ❌ BAD: crashes on null
.value | tonumber > 0

# ✅ GOOD: fallback + try/catch guards against non-numeric
(.value // "0") | (try tonumber catch 0) > 0
```
Comment thread
BillyOutlast marked this conversation as resolved.

## Bash Pipeline Traps

```bash
# ❌ BAD: while loop runs in subshell — COMMENT_BODY mutations lost
echo "$data" | jq -c '.[]' | while read -r entry; do
COMMENT_BODY+="processed"
done

# ✅ GOOD: process substitution keeps while in parent shell
while read -r entry; do
COMMENT_BODY+="processed"
done < <(echo "$data" | jq -c '.[]')
```

## SonarCloud Coverage Disconnect

`new_uncovered_lines` = 0 but `new_coverage` = 0% happens when changed lines aren't classified as "coverable" (imports, type annotations, comments). Include BOTH metrics in coverage gap detection:

```jq
select(
(.measures[]? | select(.metric == "new_uncovered_lines") | (.value // "0") | tonumber > 0)
or
(.measures[]? | select(.metric == "new_coverage") | .value // "100") == "0.0"
)
```

## Pre-commit Hook Structure

```text
1. lint-staged (prettier --write, eslint --fix) → auto-fixes JS/TS/Vue → re-stages
2. cargo fmt → auto-fixes Rust → re-stages
3. typecheck → validates (read-only, no fixes)
4. shellcheck → validates (read-only)
5. fallow audit → validates (read-only, gate-only)
```

Auto-fixers first, validators last. Nothing unfixed leaves the hook.
81 changes: 81 additions & 0 deletions .opencode/skills/pr-review-cleanup/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: pr-review-cleanup
description: Use when PR has 10+ open review threads from automated bots (OCR, CodeRabbit, Sourcery) that need batch evaluation and resolution, or when pre-push hook warnings show unresolved threads accumulating across commits.
---

# PR Review Cleanup

Automated review bots (OCR, CodeRabbit, Sourcery) fire on every push. Each push creates new threads. Without cleanup, threads accumulate exponentially — every new scan finds existing threads plus new ones.

## Core Pattern

```
push → new review run → new threads → unresolved count grows
hook warns (advisory) → agent reads JSON → batch evaluate → resolve all
```

## Quick Reference

| Step | Command |
|------|---------|
| Pull unresolved threads | `gh api graphql -f query='...reviewThreads...' -F pr=N` |
| Filter for JSON | `jq 'select(.isResolved == false)'` |
| Resolve thread | `gh api --method PATCH repos/:owner/:repo/pulls/:pr/comments/:id -f state=CLOSED` (or use MCP `resolve_thread` with PRRT_xxx ID) |
| Post summary | `gh pr comment N --body "All N threads evaluated: ..."` |
Comment thread
BillyOutlast marked this conversation as resolved.

## GraphQL Query (get PRRT_xxx IDs — cursor-paginated)

```graphql
query($owner:String!, $repo:String!, $pr:Int!, $cursor:String) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$pr) {
reviewThreads(first:100, after:$cursor) {
nodes {
id isResolved isOutdated
comments(first:1) {
nodes { author { login } body path line }
}
}
pageInfo { hasNextPage endCursor }
}
}
}
}
```
Comment thread
BillyOutlast marked this conversation as resolved.

Loop until `hasNextPage` is false. The `first:100` cap silently drops threads on large PRs; cursor pagination is mandatory.

The returned `id` is the PRRT_xxx GraphQL node ID used for resolution.

## Batch Resolution Pattern

1. Pull all threads into JSON
2. Filter `isResolved == false`
3. Categorize: *fix now* (bug/security), *skip with reason* (nitpick/false-positive), *already fixed* (commit addressed)
4. Post one summary comment explaining disposition of all threads
5. Resolve all threads via `resolve_thread` API (one call each)
6. Verify with `pre-push` hook — count should be 0

## Common Thread Categories

| Category | Action |
|----------|--------|
| Bug / security / functional | Fix in code, resolve |
| OCR false positive (Nitro auto-imports, hadolint suppression) | Skip with explanation, resolve |
| Duplicate from multiple OCR runs | Resolve (one run's copy), other auto-resolved |
| Nitpick / cosmetic | Skip with reason, resolve |
| Positive feedback ("good improvement") | Acknowledge, resolve |
| Pre-existing (not introduced by PR) | Skip, resolve |
| Stale / outdated (code already changed) | Resolve as outdated |

## Red Flags

- Resolving threads without reading them — OCR finds real bugs
- Leaving "good improvement" comments unresolved — clutter
- Not posting a summary comment — future reviewers need context on why threads were closed
- Using `--no-verify` to skip the hook instead of cleaning threads

## Pre-push Hook Integration

The pre-push hook at `.husky/pre-push` queries unresolved threads. JSON output between `## PR_REVIEW_THREADS_START` / `## PR_REVIEW_THREADS_END` markers. Agent should parse this and act BEFORE pushing — clean threads first, then push clean.
Loading
Loading