@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.
- package/.claude-plugin/plugin.json +2 -1
- package/README.md +7 -3
- package/catalog/skills.json +18 -0
- package/package.json +5 -3
- package/packages/software-development/lekker-review/SKILL.md +519 -0
- package/packages/software-development/lekker-review/adapters/claude/plugin.json +5 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/SKILL.md +520 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/completeness-critic.md +21 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/conventions.md +124 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fix-verifier.md +84 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fixer.md +120 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/implementation.md +53 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/prover.md +135 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/quality.md +72 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/simplification.md +45 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/test-quality.md +170 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-logic.md +27 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-quality.md +41 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/verifier.md +295 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/artifact-page.md +143 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/context-gathering.md +162 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/fix-mode.md +329 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/github-post.md +205 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/house-rules.md +76 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/output-format.md +232 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/changed-files.sh +77 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/setup-worktree.sh +337 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/verify-fixes.sh +231 -0
- package/packages/software-development/lekker-review/fix-workflow.js +273 -0
- package/packages/software-development/lekker-review/references/agents/completeness-critic.md +21 -0
- package/packages/software-development/lekker-review/references/agents/conventions.md +124 -0
- package/packages/software-development/lekker-review/references/agents/fix-verifier.md +84 -0
- package/packages/software-development/lekker-review/references/agents/fixer.md +120 -0
- package/packages/software-development/lekker-review/references/agents/implementation.md +53 -0
- package/packages/software-development/lekker-review/references/agents/prover.md +135 -0
- package/packages/software-development/lekker-review/references/agents/quality.md +72 -0
- package/packages/software-development/lekker-review/references/agents/simplification.md +45 -0
- package/packages/software-development/lekker-review/references/agents/test-quality.md +170 -0
- package/packages/software-development/lekker-review/references/agents/triage-logic.md +27 -0
- package/packages/software-development/lekker-review/references/agents/triage-quality.md +41 -0
- package/packages/software-development/lekker-review/references/agents/verifier.md +295 -0
- package/packages/software-development/lekker-review/references/artifact-page.md +143 -0
- package/packages/software-development/lekker-review/references/context-gathering.md +162 -0
- package/packages/software-development/lekker-review/references/fix-mode.md +329 -0
- package/packages/software-development/lekker-review/references/github-post.md +205 -0
- package/packages/software-development/lekker-review/references/house-rules.md +76 -0
- package/packages/software-development/lekker-review/references/output-format.md +232 -0
- package/packages/software-development/lekker-review/scripts/changed-files.sh +77 -0
- package/packages/software-development/lekker-review/scripts/setup-worktree.sh +337 -0
- package/packages/software-development/lekker-review/scripts/verify-fixes.sh +231 -0
- package/packages/software-development/lekker-review/workflow.js +602 -0
- package/site/assets/paperbag.css +707 -0
- package/site/assets/paperbag.js +218 -0
- 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
|