@olegkoval/agent-skills 1.26.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/README.md +7 -3
  3. package/catalog/skills.json +18 -0
  4. package/package.json +5 -3
  5. package/packages/software-development/lekker-review/SKILL.md +519 -0
  6. package/packages/software-development/lekker-review/adapters/claude/plugin.json +5 -0
  7. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/SKILL.md +520 -0
  8. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/completeness-critic.md +21 -0
  9. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/conventions.md +124 -0
  10. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fix-verifier.md +84 -0
  11. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fixer.md +120 -0
  12. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/implementation.md +53 -0
  13. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/prover.md +135 -0
  14. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/quality.md +72 -0
  15. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/simplification.md +45 -0
  16. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/test-quality.md +170 -0
  17. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-logic.md +27 -0
  18. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-quality.md +41 -0
  19. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/verifier.md +295 -0
  20. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/artifact-page.md +143 -0
  21. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/context-gathering.md +162 -0
  22. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/fix-mode.md +329 -0
  23. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/github-post.md +205 -0
  24. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/house-rules.md +76 -0
  25. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/output-format.md +232 -0
  26. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/changed-files.sh +77 -0
  27. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/setup-worktree.sh +337 -0
  28. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/verify-fixes.sh +231 -0
  29. package/packages/software-development/lekker-review/fix-workflow.js +273 -0
  30. package/packages/software-development/lekker-review/references/agents/completeness-critic.md +21 -0
  31. package/packages/software-development/lekker-review/references/agents/conventions.md +124 -0
  32. package/packages/software-development/lekker-review/references/agents/fix-verifier.md +84 -0
  33. package/packages/software-development/lekker-review/references/agents/fixer.md +120 -0
  34. package/packages/software-development/lekker-review/references/agents/implementation.md +53 -0
  35. package/packages/software-development/lekker-review/references/agents/prover.md +135 -0
  36. package/packages/software-development/lekker-review/references/agents/quality.md +72 -0
  37. package/packages/software-development/lekker-review/references/agents/simplification.md +45 -0
  38. package/packages/software-development/lekker-review/references/agents/test-quality.md +170 -0
  39. package/packages/software-development/lekker-review/references/agents/triage-logic.md +27 -0
  40. package/packages/software-development/lekker-review/references/agents/triage-quality.md +41 -0
  41. package/packages/software-development/lekker-review/references/agents/verifier.md +295 -0
  42. package/packages/software-development/lekker-review/references/artifact-page.md +143 -0
  43. package/packages/software-development/lekker-review/references/context-gathering.md +162 -0
  44. package/packages/software-development/lekker-review/references/fix-mode.md +329 -0
  45. package/packages/software-development/lekker-review/references/github-post.md +205 -0
  46. package/packages/software-development/lekker-review/references/house-rules.md +76 -0
  47. package/packages/software-development/lekker-review/references/output-format.md +232 -0
  48. package/packages/software-development/lekker-review/scripts/changed-files.sh +77 -0
  49. package/packages/software-development/lekker-review/scripts/setup-worktree.sh +337 -0
  50. package/packages/software-development/lekker-review/scripts/verify-fixes.sh +231 -0
  51. package/packages/software-development/lekker-review/workflow.js +602 -0
  52. package/site/assets/paperbag.css +707 -0
  53. package/site/assets/paperbag.js +218 -0
  54. package/site/build.mjs +380 -0
@@ -0,0 +1,232 @@
1
+ # Output format (Step 4)
2
+
3
+ ## Step 4 — Format Output
4
+
5
+ Output is **one unified markdown document** — detailed enough to stand on its
6
+ own, clean enough to paste directly into GitHub. No ANSI split, no stripped
7
+ copy-paste block. Every finding includes location, the bad code, the reason,
8
+ and the fix — all in standard GitHub markdown.
9
+
10
+ Output the review as a markdown response (not via printf). No ANSI escapes.
11
+
12
+ ---
13
+
14
+ ### Output format
15
+
16
+ ```
17
+ ## Review — [PR #<PR_NUMBER> — <title>](<PR_URL>)
18
+
19
+ **Author:** <author> · **Branch:** `<headRefName>` → `<baseRefName>`
20
+ **Size:** +<additions>/-<deletions> across <changedFiles> files
21
+ **Head:** `<headRefOid short sha>`
22
+ **Linear:** [<ticket-id>](<linear-url>) — <ticket title>
23
+ **Findings:** <X Critical / Y Important / Z Observations / W Idiomatic>
24
+ **Proofs:** <M proven / N attempted> *(include only when N > 0)*
25
+ **CI:** <✅ All passing | ⚠️ N failing: check-name | ⏳ Pending | N/A>
26
+ **Depth:** <⚡ scan | 🔍 medium | 🔬 deep>
27
+
28
+ ---
29
+
30
+ *(If PR_TITLE_ISSUE = true, insert this block here — before the Summary. Omit entirely if PR_TITLE_ISSUE = false.)*
31
+
32
+ > ⛔ **CANNOT MERGE — PR title missing Linear ticket prefix**
33
+ >
34
+ > The PR title must start with the Linear ticket(s) in square brackets.
35
+ > Recommended title prefix: **`<RECOMMENDED_PREFIX>`**
36
+ >
37
+ > How to fix: edit the PR title on GitHub to start with `<RECOMMENDED_PREFIX> <rest of current title>`.
38
+
39
+ ---
40
+
41
+ ## 🔁 Since last review
42
+
43
+ *(Re-review mode only — include when PREV_SHA was found. Omit entirely on first review.)*
44
+
45
+ Reviewed delta: `<PREV_SHA short>..<head short>`
46
+
47
+ - ✅ Fixed: <prior finding title> (`file:line`)
48
+ - ⚠️ Still open: <prior finding title> (`file:line`) — <one line on current state>
49
+
50
+ ---
51
+
52
+ ### Summary
53
+
54
+ A substantive 2–3 paragraph narrative — NOT a one-line recap. A reader who
55
+ hasn't seen the ticket or the diff should finish the Summary understanding
56
+ *why this work exists* and *what it buys the business/team*, before any finding.
57
+ Cover, in roughly this order:
58
+
59
+ - **The problem / goal — why this PR exists.** What was broken, missing, or
60
+ needed? Pull this from the Linear ticket (the AC_LIST and ticket summary),
61
+ not just the diff. State the user-facing or business outcome the change is
62
+ reaching for ("RSL can create P21 contacts automatically from Shopify instead
63
+ of hand-keying them", "unblocks the PUT/update flow that depends on the
64
+ metafield being populated").
65
+ - **What it does — the mechanism, in plain terms.** The shape of the solution:
66
+ the key entry points, the data flow, the gating/safety mechanisms, and how the
67
+ pieces fit. Name the design choices and *why each one was made* — kill switch
68
+ (so it can ship dark and be flipped per-environment), idempotency gate (so
69
+ re-deliveries don't duplicate), validation (so bad payloads fail fast and
70
+ non-retryably), stacking/deferral (so concurrency lands in a reviewable second
71
+ PR). The reader should learn the rationale, not just the inventory.
72
+ - **Risk level + recommendation.** Land on the overall risk and what you advise:
73
+ ship as-is, ship after the Important fixes, or hold. If the headline risk is a
74
+ single finding, name it here in one sentence and point forward to it.
75
+
76
+ Lead with the goal and the mechanism; keep the risk framing honest. This section
77
+ sets the altitude for everything below — a risky PR should read as risky. Do NOT
78
+ editorialize about what the PR does *well*: no praise, no "the win", no
79
+ "cleanly/nicely implemented", no complimenting the design. State what it does and
80
+ what the risks are — nothing about how good it is.
81
+
82
+ ---
83
+
84
+ **Verdict: <✅ LGTM — ship it | ⚠️ LGTM with changes | 🚫 Needs work>**
85
+
86
+ ---
87
+
88
+ ## 🔥 Production Signals
89
+
90
+ *(Include only when SENTRY_SIGNALS has entries. Omit section entirely if empty.)*
91
+
92
+ - [<issue title>](<sentry-url>) — <N> events / <M> users (7d) · first seen <date>
93
+ Files: <relevant files from stack trace>
94
+ Note: <does this PR fix, worsen, or not affect this error?>
95
+
96
+ ---
97
+
98
+ ## 🚨 Critical — must fix before merge
99
+
100
+ ### #1 — <short label>
101
+
102
+ **File:** `<path/to/file.ts>:<line>` · **Risk:** <one sentence — what breaks and when>
103
+
104
+ **Current code:**
105
+ \```ts
106
+ <the actual bad lines from the diff, verbatim>
107
+ \```
108
+
109
+ **Why it's wrong:** <explanation — what the code does vs what it should do.
110
+ Include the concrete failure: "lookup always returns undefined because the key
111
+ is triple-wrapped", "crashes with TypeError on Node 20 because Map.groupBy
112
+ does not exist", etc. If the author might not believe it, include a short
113
+ proof inline.>
114
+
115
+ **Fix:**
116
+ \```ts
117
+ <corrected version — exact drop-in replacement or minimal diff>
118
+ \```
119
+
120
+ **Proof — ran in the worktree:** *(include only when the finding carries a `proof` with `proven: true`)*
121
+ \```
122
+ <redOutput, verbatim>
123
+ \```
124
+ *Test asserts the correct behavior and fails because of this bug. It flips green when fixed.*
125
+
126
+ ---
127
+
128
+ ### #N — <label>
129
+ ...
130
+
131
+ *(None.)* ← only if section is empty
132
+
133
+ ---
134
+
135
+ ## ⚠️ Important — should fix
136
+
137
+ (same per-finding format, continuing the numbering sequence)
138
+
139
+ *(None.)* ← only if section is empty
140
+
141
+ ---
142
+
143
+ ## 📝 Observations
144
+
145
+ - <note — tradeoff, risk to watch, or design question worth raising>
146
+
147
+ *(Omit section entirely if empty.)*
148
+
149
+ ## 🧪 Test Quality
150
+
151
+ **Coverage verdict:** <✅ Well tested | ⚠️ Partially tested | ❌ Under-tested | N/A — no logic changes>
152
+
153
+ <One paragraph: overall assessment — what is tested, what is missing, and
154
+ whether the test suite would catch a regressing change to this code.>
155
+
156
+ **Gaps:**
157
+ - `test-file:line` — <gap or weakness>
158
+ - `test-file:line` — <implementation-coupled test: IO mocked to reach logic,
159
+ call-shape spy, component mocked for its props, query hook mocked, or an
160
+ implementation-detail assertion>
161
+ → **Fix:** <no-mock refactor — usually "extract a pure function over plain
162
+ data and test that" or "inject a simple fake and assert the output">
163
+
164
+ *(Omit if no mocking issues. Do not flag legitimate mocks — error paths,
165
+ pinned non-determinism, un-runnable dependencies.)*
166
+
167
+ **Mutation-slip risk:** <paragraph — which mutation classes would go undetected,
168
+ if any. "No obvious gaps found." if clean.>
169
+
170
+ *(Omit section entirely if no test changes and no business logic changes.)*
171
+
172
+ ---
173
+
174
+ ## 💡 Idiomatic & Consistency
175
+
176
+ - **<deviation>** (`<file>:<line>`) — <the idiomatic alternative in one line>.
177
+ Precedent: `<file>:<line>` where the established pattern already lives.
178
+
179
+ *(Non-blocking suggestions — do NOT affect the verdict. Omit section entirely
180
+ if empty.)*
181
+
182
+ ---
183
+
184
+ ## 💰 Review Cost
185
+
186
+ ```
187
+ Depth: <⚡ scan | 🔍 medium | 🔬 deep>
188
+ Diff size: ~<N> lines (~<N> tokens)
189
+ Agents run: <N total> — <breakdown, e.g. "5 reviewers (sonnet) + 6 verifiers (sonnet) + 2 provers (sonnet) + 1 critic (sonnet)">
190
+ Verify skipped: <N> hard-rule finding(s) exempt from adversarial verification (omit the line when 0)
191
+ Context sources: <the subset of issue-tracker / chat / docs / framework-docs / monitoring / CI / prior-review-memory actually used>
192
+
193
+ Output tokens: <N> ← ACTUAL workflow spend, from the workflow's outputTokens return value
194
+ Turn total: <N> ← whole-turn shared pool (turnTokensTotal), main loop included
195
+ Input tokens: ~<N> ← estimated: diff tokens × agent passes + context files + prompt files
196
+
197
+ Cost: ~$<X.XX> (output actual, input estimated; blended across model tiers below)
198
+ ```
199
+
200
+ Per-MTok pricing (input/output) — verified 2026-07-07 from the claude-api reference; re-check there if models changed:
201
+ - claude-fable-5: $10 / $50
202
+ - claude-opus-4-8: $5 / $25
203
+ - claude-sonnet-4-6 / claude-sonnet-5: $3 / $15
204
+ - claude-haiku-4-5: $1 / $5
205
+
206
+ Reviewer agents, verifiers, provers, and the critic all run on sonnet; triage and housekeeping on haiku. Only the main loop (context gathering + this synthesis) runs on the session model — use the session model's actual ID for that tier.
207
+
208
+ ---
209
+
210
+ ### Finding format rules
211
+
212
+ - **File + Risk** on one line — the reader knows instantly what breaks and why
213
+ it matters before reading the proof.
214
+ - **Current code** block: verbatim lines from the diff, enough context to find
215
+ the spot (3–10 lines). Never paraphrase — paste the actual code.
216
+ - **Why it's wrong**: lead with the failure mode ("this always returns false
217
+ because…"), then the mechanism. Keep to 3–6 sentences. If a comparison or
218
+ counter-example makes it clearer, include it.
219
+ - **Fix** block: show the corrected version as a drop-in replacement. When the
220
+ fix is architectural (extract helper, add index), describe it concisely then
221
+ show a minimal skeleton if useful. If the fix touches consumers outside the
222
+ changed file, call that out explicitly — never show a partial fix that looks
223
+ self-contained but silently breaks callers.
224
+ - Blank line between findings. No double blank lines.
225
+ - No trailing whitespace, no HTML tags, no ANSI escapes.
226
+ - **Proof counter-evidence rule**: when `proof.attempted` is true but `proven`
227
+ is false because the code behaved correctly for the tested input, the
228
+ synthesis MUST treat that as counter-evidence — either downgrade the finding
229
+ or state in the finding body why the proof attempt doesn't exonerate it
230
+ (e.g. the tested input wasn't the one that actually breaks). A finding whose
231
+ proof came back green cannot silently stay Critical. When `attempted` is
232
+ false, say nothing — untestable is not evidence either way.
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # changed-files.sh -- lekker-review helper
3
+ # Determines the base ref for the branch checked out in WORKTREE_PATH and
4
+ # prints, one path per line on stdout, the files this branch has changed
5
+ # relative to that base. Prints the base ref actually used to stderr,
6
+ # prefixed "base-ref: ", so callers can capture it separately from the file
7
+ # list.
8
+ #
9
+ # Usage: changed-files.sh WORKTREE_PATH [--include-uncommitted]
10
+ # --include-uncommitted: also union in the worktree's uncommitted (working
11
+ # tree + staged) changes -- what a fix agent just wrote, even if it wasn't
12
+ # part of the original PR diff.
13
+ #
14
+ # Never fails: if no base ref can be resolved (or merge-base fails), prints
15
+ # NOTHING to stdout, "base-ref: unknown" to stderr, and exits 0. Callers
16
+ # should treat empty stdout as "could not scope -- fall back to repo-wide".
17
+
18
+ set -uo pipefail
19
+
20
+ WORKTREE_PATH="${1:?WORKTREE_PATH required}"
21
+ INCLUDE_UNCOMMITTED=false
22
+ [[ "${2:-}" == "--include-uncommitted" ]] && INCLUDE_UNCOMMITTED=true
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # 1. Resolve a base ref: origin's default branch first, then a fixed
26
+ # fallback order.
27
+ # ---------------------------------------------------------------------------
28
+ BASE_REF=""
29
+
30
+ SYM_REF="$(git -C "$WORKTREE_PATH" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true)"
31
+ if [[ -n "$SYM_REF" ]]; then
32
+ candidate="origin/${SYM_REF#refs/remotes/origin/}"
33
+ if git -C "$WORKTREE_PATH" rev-parse --verify --quiet "$candidate" >/dev/null 2>&1; then
34
+ BASE_REF="$candidate"
35
+ fi
36
+ fi
37
+
38
+ if [[ -z "$BASE_REF" ]]; then
39
+ for candidate in origin/staging origin/main origin/master; do
40
+ if git -C "$WORKTREE_PATH" rev-parse --verify --quiet "$candidate" >/dev/null 2>&1; then
41
+ BASE_REF="$candidate"
42
+ break
43
+ fi
44
+ done
45
+ fi
46
+
47
+ if [[ -z "$BASE_REF" ]]; then
48
+ printf 'base-ref: unknown\n' >&2
49
+ exit 0
50
+ fi
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # 2. Merge-base + diff.
54
+ # ---------------------------------------------------------------------------
55
+ MERGE_BASE="$(git -C "$WORKTREE_PATH" merge-base HEAD "$BASE_REF" 2>/dev/null || true)"
56
+ if [[ -z "$MERGE_BASE" ]]; then
57
+ printf 'base-ref: unknown\n' >&2
58
+ exit 0
59
+ fi
60
+
61
+ printf 'base-ref: %s\n' "$BASE_REF" >&2
62
+
63
+ CHANGED_TMP="/tmp/lekker-changed-files-tmp-$$.txt"
64
+ git -C "$WORKTREE_PATH" diff --name-only "$MERGE_BASE" HEAD > "$CHANGED_TMP" 2>/dev/null || true
65
+
66
+ if [[ "$INCLUDE_UNCOMMITTED" == "true" ]]; then
67
+ git -C "$WORKTREE_PATH" diff --name-only HEAD >> "$CHANGED_TMP" 2>/dev/null || true
68
+ git -C "$WORKTREE_PATH" diff --name-only --cached >> "$CHANGED_TMP" 2>/dev/null || true
69
+ # Untracked-but-not-ignored files too: a fix agent that creates a new file
70
+ # (extracted helper, new test) and does not stage it is invisible to both
71
+ # diffs above, so its type/lint errors would escape attribution entirely.
72
+ git -C "$WORKTREE_PATH" ls-files --others --exclude-standard >> "$CHANGED_TMP" 2>/dev/null || true
73
+ sort -u "$CHANGED_TMP" -o "$CHANGED_TMP" 2>/dev/null || true
74
+ fi
75
+
76
+ cat "$CHANGED_TMP"
77
+ rm -f "$CHANGED_TMP"
@@ -0,0 +1,337 @@
1
+ #!/bin/bash
2
+ # setup-worktree.sh -- lekker-review helper
3
+ # Usage: setup-worktree.sh REPO_SLUG PR_BRANCH OUT_JSON [PREV_SHA]
4
+ # Emits a JSON result file at OUT_JSON and prints "OK <OUT_JSON>" on success.
5
+
6
+ set -euo pipefail
7
+
8
+ REPO_SLUG="${1:?REPO_SLUG required}"
9
+ PR_BRANCH="${2:?PR_BRANCH required}"
10
+ OUT_JSON="${3:?OUT_JSON required}"
11
+ PREV_SHA="${4:-}"
12
+
13
+ CACHE_DIR="$HOME/.cache/lekker-review"
14
+ CACHE_FILE="$CACHE_DIR/repo-map.tsv"
15
+ NOTES=""
16
+ WORKTREE_PATH="/tmp/lekker-review-$$"
17
+
18
+ mkdir -p "$CACHE_DIR"
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Helper: normalise a remote URL to an org/repo comparable key.
22
+ # Strips protocol, host, and trailing .git so "github.com/org/repo" ->
23
+ # "org/repo".
24
+ # ---------------------------------------------------------------------------
25
+ normalize_url() {
26
+ local url
27
+ url="$1"
28
+ url="${url%.git}"
29
+ # Strip https://host/ prefix
30
+ url="${url#*://*/}"
31
+ # For SCP-style git@host:org/repo - strip up to and including ':'
32
+ url="${url#*:}"
33
+ printf '%s' "$url"
34
+ }
35
+
36
+ SLUG_NORM="$(normalize_url "https://github.com/${REPO_SLUG}.git")"
37
+
38
+ REPO_ROOT=""
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # 1a. Try the cache
42
+ # ---------------------------------------------------------------------------
43
+ if [[ -f "$CACHE_FILE" ]]; then
44
+ while IFS=$'\t' read -r cached_slug cached_path; do
45
+ [[ "$cached_slug" == "$REPO_SLUG" ]] || continue
46
+ if [[ -d "$cached_path" ]]; then
47
+ local_url="$(git -C "$cached_path" remote get-url origin 2>/dev/null || true)"
48
+ local_norm="$(normalize_url "$local_url")"
49
+ if [[ "$local_norm" == "$SLUG_NORM" ]]; then
50
+ REPO_ROOT="$cached_path"
51
+ fi
52
+ fi
53
+ break
54
+ done < "$CACHE_FILE"
55
+ fi
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # 1b. Try $PWD
59
+ # ---------------------------------------------------------------------------
60
+ if [[ -z "$REPO_ROOT" ]] && git -C "$PWD" remote get-url origin >/dev/null 2>&1; then
61
+ pwd_url="$(git -C "$PWD" remote get-url origin 2>/dev/null || true)"
62
+ pwd_norm="$(normalize_url "$pwd_url")"
63
+ if [[ "$pwd_norm" == "$SLUG_NORM" ]]; then
64
+ REPO_ROOT="$PWD"
65
+ fi
66
+ fi
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # 1c. Scan ~/Work (maxdepth 4)
70
+ # ---------------------------------------------------------------------------
71
+ if [[ -z "$REPO_ROOT" ]]; then
72
+ while IFS= read -r git_dir; do
73
+ candidate="${git_dir%/.git}"
74
+ [[ -d "$candidate" ]] || continue
75
+ cand_url="$(git -C "$candidate" remote get-url origin 2>/dev/null || true)"
76
+ [[ -n "$cand_url" ]] || continue
77
+ cand_norm="$(normalize_url "$cand_url")"
78
+ if [[ "$cand_norm" == "$SLUG_NORM" ]]; then
79
+ REPO_ROOT="$candidate"
80
+ break
81
+ fi
82
+ done < <(find "$HOME/Work" -maxdepth 4 -name ".git" -type d 2>/dev/null)
83
+ fi
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # 1d. Shallow clone as last resort (not cached)
87
+ # ---------------------------------------------------------------------------
88
+ if [[ -z "$REPO_ROOT" ]]; then
89
+ CLONE_DIR="/tmp/lekker-clone-$$"
90
+ git clone --depth=100 "https://github.com/${REPO_SLUG}.git" "$CLONE_DIR"
91
+ REPO_ROOT="$CLONE_DIR"
92
+ NOTES="${NOTES}cloned-shallow;"
93
+ else
94
+ # Refresh cache entry: remove stale line and append fresh one
95
+ if [[ -f "$CACHE_FILE" ]]; then
96
+ grep -v "^${REPO_SLUG} " "$CACHE_FILE" > "${CACHE_FILE}.tmp" 2>/dev/null || true
97
+ mv "${CACHE_FILE}.tmp" "$CACHE_FILE"
98
+ fi
99
+ printf '%s\t%s\n' "$REPO_SLUG" "$REPO_ROOT" >> "$CACHE_FILE"
100
+ fi
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # 2. Fetch + worktree
104
+ # ---------------------------------------------------------------------------
105
+ git -C "$REPO_ROOT" fetch origin "$PR_BRANCH" 2>/dev/null || true
106
+
107
+ if ! git -C "$REPO_ROOT" worktree add "$WORKTREE_PATH" "origin/$PR_BRANCH" 2>/dev/null; then
108
+ if ! git -C "$REPO_ROOT" worktree add "$WORKTREE_PATH" FETCH_HEAD 2>/dev/null; then
109
+ printf 'ERROR: could not create worktree at %s\n' "$WORKTREE_PATH" >&2
110
+ exit 1
111
+ fi
112
+ NOTES="${NOTES}worktree-used-FETCH_HEAD;"
113
+ fi
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # 3. node_modules symlink
117
+ # ---------------------------------------------------------------------------
118
+ NM_LINKED=false
119
+ if [[ -d "$REPO_ROOT/node_modules" ]]; then
120
+ ln -s "$REPO_ROOT/node_modules" "$WORKTREE_PATH/node_modules"
121
+ NM_LINKED=true
122
+ else
123
+ NOTES="${NOTES}no-node_modules-static-checks-skipped;"
124
+ fi
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # 3b. Determine changed files (to scope tsc/eslint to this PR's diff instead
128
+ # of the whole repo -- see scripts/changed-files.sh).
129
+ # ---------------------------------------------------------------------------
130
+ CHANGED_FILES_FILE="/tmp/lekker-changed-$$.txt"
131
+ BASE_REF_NOTE="$("$(dirname "$0")/changed-files.sh" "$WORKTREE_PATH" 2>&1 >"$CHANGED_FILES_FILE" | sed -n 's/^base-ref: //p')" || true
132
+
133
+ # A blank line here would make `grep -F -f` match EVERY tsc error line, turning
134
+ # the repo's standing type debt into "errors this PR introduced". Strip them.
135
+ if [[ -f "$CHANGED_FILES_FILE" ]]; then
136
+ grep -v '^[[:space:]]*$' "$CHANGED_FILES_FILE" > "${CHANGED_FILES_FILE}.clean" 2>/dev/null || true
137
+ mv "${CHANGED_FILES_FILE}.clean" "$CHANGED_FILES_FILE" 2>/dev/null || true
138
+ fi
139
+
140
+ BASE_REF_VAL="null"
141
+ if [[ -n "$BASE_REF_NOTE" ]] && [[ "$BASE_REF_NOTE" != "unknown" ]]; then
142
+ BASE_REF_VAL="$BASE_REF_NOTE"
143
+ else
144
+ NOTES="${NOTES}static-checks-repo-wide-no-base-ref;"
145
+ fi
146
+
147
+ # Lintable, still-on-disk changed files (a deleted file must not go to eslint).
148
+ ESLINT_TARGETS=()
149
+ if [[ -s "$CHANGED_FILES_FILE" ]]; then
150
+ while IFS= read -r cf; do
151
+ [[ -z "$cf" ]] && continue
152
+ case "$cf" in
153
+ *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs) ;;
154
+ *) continue ;;
155
+ esac
156
+ [[ -f "$WORKTREE_PATH/$cf" ]] || continue
157
+ ESLINT_TARGETS+=("$cf")
158
+ done < "$CHANGED_FILES_FILE"
159
+ fi
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # 4. Concurrent tsc + eslint (never fail the script)
163
+ # ---------------------------------------------------------------------------
164
+ TSC_TAIL_FILE="/tmp/lekker-tsc-$$.txt"
165
+ TSC_FULL_FILE="/tmp/lekker-tsc-full-$$.txt"
166
+ TSC_CHANGED_FILE="/tmp/lekker-tsc-changed-$$.txt"
167
+ TSC_COUNT_FILE="/tmp/lekker-tsc-count-$$.txt"
168
+ ESLINT_TAIL_FILE="/tmp/lekker-eslint-$$.txt"
169
+ ESLINT_SCOPE_FILE="/tmp/lekker-eslint-scope-$$.txt"
170
+ printf '' > "$TSC_TAIL_FILE"
171
+ printf '' > "$TSC_CHANGED_FILE"
172
+ printf '0' > "$TSC_COUNT_FILE"
173
+ printf '' > "$ESLINT_TAIL_FILE"
174
+ printf 'skipped' > "$ESLINT_SCOPE_FILE"
175
+
176
+ run_tsc() {
177
+ # tsc needs the whole program to resolve types, so it always runs
178
+ # repo-wide; only the reported OUTPUT is split by changed-file scope.
179
+ if [[ -f "$WORKTREE_PATH/tsconfig.json" ]] && [[ "$NM_LINKED" == "true" ]]; then
180
+ (cd "$WORKTREE_PATH" && npx tsc --noEmit) > "$TSC_FULL_FILE" 2>&1 || true
181
+ tail -40 "$TSC_FULL_FILE" >> "$TSC_TAIL_FILE" || true
182
+ grep -c 'error TS' "$TSC_FULL_FILE" > "$TSC_COUNT_FILE" 2>/dev/null || printf '0' > "$TSC_COUNT_FILE"
183
+ if [[ -s "$CHANGED_FILES_FILE" ]]; then
184
+ grep -F -f "$CHANGED_FILES_FILE" "$TSC_FULL_FILE" 2>/dev/null | tail -40 > "$TSC_CHANGED_FILE" || true
185
+ fi
186
+ rm -f "$TSC_FULL_FILE"
187
+ else
188
+ printf 'skipped\n' > "$TSC_TAIL_FILE"
189
+ printf '0' > "$TSC_COUNT_FILE"
190
+ fi
191
+ }
192
+
193
+ run_eslint() {
194
+ local has_config
195
+ has_config=false
196
+ if [[ "$NM_LINKED" == "true" ]]; then
197
+ (cd "$WORKTREE_PATH" && compgen -G '.eslintrc*' >/dev/null 2>&1) && has_config=true
198
+ if [[ "$has_config" == "false" ]]; then
199
+ (cd "$WORKTREE_PATH" && compgen -G 'eslint.config*' >/dev/null 2>&1) && has_config=true
200
+ fi
201
+ if [[ "$has_config" == "true" ]]; then
202
+ if [[ -s "$CHANGED_FILES_FILE" ]]; then
203
+ if [[ ${#ESLINT_TARGETS[@]} -gt 0 ]]; then
204
+ # Default (stylish) formatter: ESLint 9 removed the bundled "compact" formatter
205
+ (cd "$WORKTREE_PATH" && npx eslint --max-warnings=0 "${ESLINT_TARGETS[@]}" 2>&1 | tail -40 >> "$ESLINT_TAIL_FILE") || true
206
+ printf 'changed-files' > "$ESLINT_SCOPE_FILE"
207
+ else
208
+ printf 'no-changed-lintable-files\n' > "$ESLINT_TAIL_FILE"
209
+ printf 'none' > "$ESLINT_SCOPE_FILE"
210
+ fi
211
+ else
212
+ # Base ref could not be resolved -- fall back to the old repo-wide run.
213
+ (cd "$WORKTREE_PATH" && npx eslint . --max-warnings=0 2>&1 | tail -40 >> "$ESLINT_TAIL_FILE") || true
214
+ printf 'full-fallback' > "$ESLINT_SCOPE_FILE"
215
+ fi
216
+ else
217
+ printf 'no-eslint-config\n' > "$ESLINT_TAIL_FILE"
218
+ printf 'no-eslint-config' > "$ESLINT_SCOPE_FILE"
219
+ fi
220
+ else
221
+ printf 'skipped\n' > "$ESLINT_TAIL_FILE"
222
+ printf 'skipped' > "$ESLINT_SCOPE_FILE"
223
+ fi
224
+ }
225
+
226
+ run_tsc &
227
+ TSC_PID=$!
228
+ run_eslint &
229
+ ESLINT_PID=$!
230
+ wait "$TSC_PID" || true
231
+ wait "$ESLINT_PID" || true
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # 5. Collect PROJECT_RULES from CLAUDE.md files (depth <= 3, skip node_modules)
235
+ # ---------------------------------------------------------------------------
236
+ PROJECT_RULES_FILE="/tmp/lekker-rules-$$.txt"
237
+ printf '' > "$PROJECT_RULES_FILE"
238
+
239
+ while IFS= read -r claude_md; do
240
+ printf '=== %s ===\n' "$claude_md" >> "$PROJECT_RULES_FILE"
241
+ cat "$claude_md" >> "$PROJECT_RULES_FILE"
242
+ printf '\n' >> "$PROJECT_RULES_FILE"
243
+ done < <(find "$WORKTREE_PATH" -maxdepth 3 -name 'CLAUDE.md' \
244
+ -not -path '*/node_modules/*' 2>/dev/null)
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # 6. HEAD SHA
248
+ # ---------------------------------------------------------------------------
249
+ HEAD_SHA="$(git -C "$WORKTREE_PATH" rev-parse HEAD)"
250
+ HEAD_SHA_SHORT="$(git -C "$WORKTREE_PATH" rev-parse --short HEAD)"
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # 7. Delta diff (PREV_SHA)
254
+ # ---------------------------------------------------------------------------
255
+ DELTA_FILE_VAL="null"
256
+ if [[ -n "$PREV_SHA" ]]; then
257
+ if git -C "$WORKTREE_PATH" cat-file -e "$PREV_SHA" 2>/dev/null; then
258
+ DELTA_FILE_PATH="/tmp/lekker-delta-$$.diff"
259
+ git -C "$WORKTREE_PATH" diff "${PREV_SHA}..HEAD" > "$DELTA_FILE_PATH"
260
+ DELTA_FILE_VAL="$DELTA_FILE_PATH"
261
+ else
262
+ NOTES="${NOTES}prev-sha-not-found-full-review;"
263
+ fi
264
+ fi
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # 8. Emit JSON via python3 (safe escaping via json.dump)
268
+ # All values passed as argv to avoid any shell-escaping in the heredoc.
269
+ # ---------------------------------------------------------------------------
270
+ python3 - \
271
+ "$WORKTREE_PATH" \
272
+ "$REPO_ROOT" \
273
+ "$HEAD_SHA" \
274
+ "$HEAD_SHA_SHORT" \
275
+ "$TSC_TAIL_FILE" \
276
+ "$ESLINT_TAIL_FILE" \
277
+ "$PROJECT_RULES_FILE" \
278
+ "$DELTA_FILE_VAL" \
279
+ "$NOTES" \
280
+ "$CHANGED_FILES_FILE" \
281
+ "$BASE_REF_VAL" \
282
+ "$ESLINT_SCOPE_FILE" \
283
+ "$TSC_CHANGED_FILE" \
284
+ "$TSC_COUNT_FILE" \
285
+ "$OUT_JSON" <<'PYEOF'
286
+ import json, sys, os
287
+
288
+ (worktree_path, repo_root, head_sha, head_sha_short,
289
+ tsc_file, eslint_file, rules_file, delta_file, notes,
290
+ changed_files_file, base_ref_val, eslint_scope_file,
291
+ tsc_changed_file, tsc_count_file,
292
+ out_json) = sys.argv[1:16]
293
+
294
+ def read_file(path):
295
+ if not os.path.exists(path):
296
+ return ""
297
+ with open(path, "r", errors="replace") as f:
298
+ return f.read()
299
+
300
+ changed_files = [l for l in read_file(changed_files_file).splitlines() if l.strip()]
301
+
302
+ eslint_scope = read_file(eslint_scope_file).strip() or "skipped"
303
+
304
+ try:
305
+ tsc_error_count = int(read_file(tsc_count_file).strip() or "0")
306
+ except ValueError:
307
+ tsc_error_count = 0
308
+
309
+ data = {
310
+ "worktreePath": worktree_path,
311
+ "repoRoot": repo_root,
312
+ "headSha": head_sha,
313
+ "headShaShort": head_sha_short,
314
+ "tscTail": read_file(tsc_file),
315
+ "eslintTail": read_file(eslint_file),
316
+ "projectRules": read_file(rules_file),
317
+ "deltaFile": None if delta_file == "null" else delta_file,
318
+ "notes": notes,
319
+ "changedFiles": changed_files,
320
+ "baseRef": None if base_ref_val == "null" else base_ref_val,
321
+ "eslintScope": eslint_scope,
322
+ "tscChangedTail": read_file(tsc_changed_file),
323
+ "tscErrorCount": tsc_error_count,
324
+ }
325
+
326
+ for p in (tsc_file, eslint_file, rules_file, changed_files_file,
327
+ eslint_scope_file, tsc_changed_file, tsc_count_file):
328
+ try:
329
+ os.remove(p)
330
+ except OSError:
331
+ pass
332
+
333
+ with open(out_json, "w") as f:
334
+ json.dump(data, f, indent=2)
335
+
336
+ print("OK " + out_json)
337
+ PYEOF