@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,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
@@ -0,0 +1,231 @@
1
+ #!/bin/bash
2
+ # verify-fixes.sh -- lekker-review fix-mode helper
3
+ # Runs static checks + (optionally) the test suite against the worktree AFTER
4
+ # fix agents have edited it, and emits a JSON result file.
5
+ #
6
+ # Usage: verify-fixes.sh WORKTREE_PATH OUT_JSON [RUN_TESTS]
7
+ # RUN_TESTS: "tests" to run the package test script, anything else to skip.
8
+ #
9
+ # Never fails on a failing check -- a failing check is data, not an error.
10
+ # Exit code is non-zero only when the worktree itself is unusable.
11
+
12
+ set -uo pipefail
13
+
14
+ WORKTREE_PATH="${1:?WORKTREE_PATH required}"
15
+ OUT_JSON="${2:?OUT_JSON required}"
16
+ RUN_TESTS="${3:-no-tests}"
17
+
18
+ if [[ ! -d "$WORKTREE_PATH/.git" ]] && [[ ! -f "$WORKTREE_PATH/.git" ]]; then
19
+ printf 'ERROR: %s is not a git worktree\n' "$WORKTREE_PATH" >&2
20
+ exit 1
21
+ fi
22
+
23
+ TSC_FILE="/tmp/lekker-fix-tsc-$$.txt"
24
+ TSC_FULL_FILE="/tmp/lekker-fix-tsc-full-$$.txt"
25
+ TSC_CHANGED_FILE="/tmp/lekker-fix-tsc-changed-$$.txt"
26
+ TSC_COUNT_FILE="/tmp/lekker-fix-tsc-count-$$.txt"
27
+ ESLINT_FILE="/tmp/lekker-fix-eslint-$$.txt"
28
+ ESLINT_SCOPE_FILE="/tmp/lekker-fix-eslint-scope-$$.txt"
29
+ TEST_FILE="/tmp/lekker-fix-test-$$.txt"
30
+ STAT_FILE="/tmp/lekker-fix-stat-$$.txt"
31
+ printf '' > "$TSC_FILE"
32
+ printf '' > "$TSC_CHANGED_FILE"
33
+ printf '0' > "$TSC_COUNT_FILE"
34
+ printf '' > "$ESLINT_FILE"
35
+ printf 'skipped' > "$ESLINT_SCOPE_FILE"
36
+ printf '' > "$TEST_FILE"
37
+
38
+ HAS_NM=false
39
+ [[ -d "$WORKTREE_PATH/node_modules" || -L "$WORKTREE_PATH/node_modules" ]] && HAS_NM=true
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Uncommitted change footprint (what the fix agents actually wrote)
43
+ # ---------------------------------------------------------------------------
44
+ git -C "$WORKTREE_PATH" diff --stat > "$STAT_FILE" 2>&1 || true
45
+ DIRTY="$(git -C "$WORKTREE_PATH" status --porcelain 2>/dev/null || true)"
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Determine changed files (to scope tsc/eslint identically to setup-worktree
49
+ # so fix-mode's post-fix numbers compare like-for-like against the baseline).
50
+ # --include-uncommitted also picks up what the fix agents just wrote, even if
51
+ # it wasn't part of the original PR diff.
52
+ # ---------------------------------------------------------------------------
53
+ CHANGED_FILES_FILE="/tmp/lekker-fix-changed-$$.txt"
54
+ BASE_REF_NOTE="$("$(dirname "$0")/changed-files.sh" "$WORKTREE_PATH" --include-uncommitted 2>&1 >"$CHANGED_FILES_FILE" | sed -n 's/^base-ref: //p')" || true
55
+
56
+ # A blank line here would make `grep -F -f` match EVERY tsc error line, turning
57
+ # the repo's standing type debt into "errors the fixes introduced". Strip them.
58
+ if [[ -f "$CHANGED_FILES_FILE" ]]; then
59
+ grep -v '^[[:space:]]*$' "$CHANGED_FILES_FILE" > "${CHANGED_FILES_FILE}.clean" 2>/dev/null || true
60
+ mv "${CHANGED_FILES_FILE}.clean" "$CHANGED_FILES_FILE" 2>/dev/null || true
61
+ fi
62
+
63
+ BASE_REF_VAL="null"
64
+ if [[ -n "$BASE_REF_NOTE" ]] && [[ "$BASE_REF_NOTE" != "unknown" ]]; then
65
+ BASE_REF_VAL="$BASE_REF_NOTE"
66
+ fi
67
+
68
+ # Lintable, still-on-disk changed files (a deleted file must not go to eslint).
69
+ ESLINT_TARGETS=()
70
+ if [[ -s "$CHANGED_FILES_FILE" ]]; then
71
+ while IFS= read -r cf; do
72
+ [[ -z "$cf" ]] && continue
73
+ case "$cf" in
74
+ *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs) ;;
75
+ *) continue ;;
76
+ esac
77
+ [[ -f "$WORKTREE_PATH/$cf" ]] || continue
78
+ ESLINT_TARGETS+=("$cf")
79
+ done < "$CHANGED_FILES_FILE"
80
+ fi
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # tsc + eslint (concurrent)
84
+ # ---------------------------------------------------------------------------
85
+ run_tsc() {
86
+ # tsc needs the whole program to resolve types, so it always runs
87
+ # repo-wide; only the reported OUTPUT is split by changed-file scope.
88
+ if [[ -f "$WORKTREE_PATH/tsconfig.json" ]] && [[ "$HAS_NM" == "true" ]]; then
89
+ (cd "$WORKTREE_PATH" && npx tsc --noEmit) > "$TSC_FULL_FILE" 2>&1 || true
90
+ tail -40 "$TSC_FULL_FILE" >> "$TSC_FILE" || true
91
+ grep -c 'error TS' "$TSC_FULL_FILE" > "$TSC_COUNT_FILE" 2>/dev/null || printf '0' > "$TSC_COUNT_FILE"
92
+ if [[ -s "$CHANGED_FILES_FILE" ]]; then
93
+ grep -F -f "$CHANGED_FILES_FILE" "$TSC_FULL_FILE" 2>/dev/null | tail -40 > "$TSC_CHANGED_FILE" || true
94
+ fi
95
+ rm -f "$TSC_FULL_FILE"
96
+ else
97
+ printf 'skipped\n' > "$TSC_FILE"
98
+ printf '0' > "$TSC_COUNT_FILE"
99
+ fi
100
+ }
101
+
102
+ run_eslint() {
103
+ if [[ "$HAS_NM" != "true" ]]; then
104
+ printf 'skipped\n' > "$ESLINT_FILE"
105
+ printf 'skipped' > "$ESLINT_SCOPE_FILE"
106
+ return
107
+ fi
108
+ local has_config
109
+ has_config=false
110
+ (cd "$WORKTREE_PATH" && compgen -G '.eslintrc*' >/dev/null 2>&1) && has_config=true
111
+ if [[ "$has_config" == "false" ]]; then
112
+ (cd "$WORKTREE_PATH" && compgen -G 'eslint.config*' >/dev/null 2>&1) && has_config=true
113
+ fi
114
+ if [[ "$has_config" == "true" ]]; then
115
+ if [[ -s "$CHANGED_FILES_FILE" ]]; then
116
+ if [[ ${#ESLINT_TARGETS[@]} -gt 0 ]]; then
117
+ (cd "$WORKTREE_PATH" && npx eslint --max-warnings=0 "${ESLINT_TARGETS[@]}" 2>&1 | tail -40 >> "$ESLINT_FILE") || true
118
+ printf 'changed-files' > "$ESLINT_SCOPE_FILE"
119
+ else
120
+ printf 'no-changed-lintable-files\n' > "$ESLINT_FILE"
121
+ printf 'none' > "$ESLINT_SCOPE_FILE"
122
+ fi
123
+ else
124
+ # Base ref could not be resolved -- fall back to the old repo-wide run.
125
+ (cd "$WORKTREE_PATH" && npx eslint . --max-warnings=0 2>&1 | tail -40 >> "$ESLINT_FILE") || true
126
+ printf 'full-fallback' > "$ESLINT_SCOPE_FILE"
127
+ fi
128
+ else
129
+ printf 'no-eslint-config\n' > "$ESLINT_FILE"
130
+ printf 'no-eslint-config' > "$ESLINT_SCOPE_FILE"
131
+ fi
132
+ }
133
+
134
+ run_tsc &
135
+ TSC_PID=$!
136
+ run_eslint &
137
+ ESLINT_PID=$!
138
+ wait "$TSC_PID" || true
139
+ wait "$ESLINT_PID" || true
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Test suite (sequential, opt-in, hard timeout)
143
+ # ---------------------------------------------------------------------------
144
+ TEST_RAN=false
145
+ TEST_EXIT="null"
146
+ if [[ "$RUN_TESTS" == "tests" ]] && [[ "$HAS_NM" == "true" ]] \
147
+ && [[ -f "$WORKTREE_PATH/package.json" ]] \
148
+ && grep -q '"test"[[:space:]]*:' "$WORKTREE_PATH/package.json"; then
149
+ TEST_RAN=true
150
+ # `set -o pipefail` is active, so the subshell's exit status already
151
+ # reflects a failing `npm test` even though tail is last in the pipeline.
152
+ if command -v timeout >/dev/null 2>&1; then
153
+ (cd "$WORKTREE_PATH" && timeout 600 npm test --silent 2>&1 | tail -60 >> "$TEST_FILE")
154
+ else
155
+ (cd "$WORKTREE_PATH" && npm test --silent 2>&1 | tail -60 >> "$TEST_FILE")
156
+ fi
157
+ TEST_EXIT=$?
158
+ else
159
+ printf 'skipped\n' > "$TEST_FILE"
160
+ fi
161
+
162
+ HEAD_SHA_SHORT="$(git -C "$WORKTREE_PATH" rev-parse --short HEAD 2>/dev/null || printf 'unknown')"
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # Emit JSON
166
+ # ---------------------------------------------------------------------------
167
+ python3 - \
168
+ "$TSC_FILE" \
169
+ "$ESLINT_FILE" \
170
+ "$TEST_FILE" \
171
+ "$STAT_FILE" \
172
+ "$TEST_RAN" \
173
+ "$TEST_EXIT" \
174
+ "$HEAD_SHA_SHORT" \
175
+ "$DIRTY" \
176
+ "$CHANGED_FILES_FILE" \
177
+ "$BASE_REF_VAL" \
178
+ "$ESLINT_SCOPE_FILE" \
179
+ "$TSC_CHANGED_FILE" \
180
+ "$TSC_COUNT_FILE" \
181
+ "$OUT_JSON" <<'PYEOF'
182
+ import json, sys, os
183
+
184
+ (tsc_file, eslint_file, test_file, stat_file, test_ran, test_exit,
185
+ head_sha_short, dirty, changed_files_file, base_ref_val,
186
+ eslint_scope_file, tsc_changed_file, tsc_count_file,
187
+ out_json) = sys.argv[1:15]
188
+
189
+ def read_file(path):
190
+ if not os.path.exists(path):
191
+ return ""
192
+ with open(path, "r", errors="replace") as f:
193
+ return f.read()
194
+
195
+ changed_files = [l for l in read_file(changed_files_file).splitlines() if l.strip()]
196
+
197
+ eslint_scope = read_file(eslint_scope_file).strip() or "skipped"
198
+
199
+ try:
200
+ tsc_error_count = int(read_file(tsc_count_file).strip() or "0")
201
+ except ValueError:
202
+ tsc_error_count = 0
203
+
204
+ data = {
205
+ "tscTail": read_file(tsc_file),
206
+ "eslintTail": read_file(eslint_file),
207
+ "testTail": read_file(test_file),
208
+ "testRan": test_ran == "true",
209
+ "testExitCode": None if test_exit in ("null", "") else int(test_exit),
210
+ "diffStat": read_file(stat_file),
211
+ "dirtyPaths": [l for l in dirty.splitlines() if l.strip()],
212
+ "headShaShort": head_sha_short,
213
+ "changedFiles": changed_files,
214
+ "baseRef": None if base_ref_val == "null" else base_ref_val,
215
+ "eslintScope": eslint_scope,
216
+ "tscChangedTail": read_file(tsc_changed_file),
217
+ "tscErrorCount": tsc_error_count,
218
+ }
219
+
220
+ for p in (tsc_file, eslint_file, test_file, stat_file, changed_files_file,
221
+ eslint_scope_file, tsc_changed_file, tsc_count_file):
222
+ try:
223
+ os.remove(p)
224
+ except OSError:
225
+ pass
226
+
227
+ with open(out_json, "w") as f:
228
+ json.dump(data, f, indent=2)
229
+
230
+ print("OK " + out_json)
231
+ PYEOF