opencode-swarm 7.164.12 → 7.165.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 (114) hide show
  1. package/.opencode/skills/durable-session-state/SKILL.md +1 -1
  2. package/.opencode/skills/issue-tracer/SKILL.md +140 -286
  3. package/.opencode/skills/issue-tracer/assets/pr-template.md +13 -7
  4. package/.opencode/skills/issue-tracer/references/acceptance-checks.md +66 -0
  5. package/.opencode/skills/issue-tracer/references/critic-gate.md +53 -11
  6. package/.opencode/skills/issue-tracer/references/evidence-artifacts.md +128 -23
  7. package/.opencode/skills/issue-tracer/references/full-resolution-contract.md +69 -0
  8. package/.opencode/skills/issue-tracer/references/install.md +31 -9
  9. package/.opencode/skills/issue-tracer/references/localization-playbook.md +26 -14
  10. package/.opencode/skills/issue-tracer/references/method-provenance.md +19 -3
  11. package/.opencode/skills/issue-tracer/references/phase-0-setup.md +90 -0
  12. package/.opencode/skills/issue-tracer/references/phase-1-intake.md +43 -0
  13. package/.opencode/skills/issue-tracer/references/untrusted-content.md +13 -5
  14. package/.opencode/skills/issue-tracer/scripts/repro-check.sh +416 -0
  15. package/.opencode/skills/issue-tracer/scripts/trace-check.sh +479 -0
  16. package/.opencode/skills/issue-tracer/scripts/trace-init.sh +96 -6
  17. package/.opencode/skills/loop/SKILL.md +1 -1
  18. package/.opencode/skills/{plan → swarm-plan}/SKILL.md +1 -1
  19. package/dist/cli/{coder-settlement-yh1q5fr3.js → coder-settlement-89nt4j7g.js} +11 -11
  20. package/dist/cli/{config-doctor-bd4yzmh7.js → config-doctor-gm6f5chm.js} +3 -3
  21. package/dist/cli/{core-y0s1vmyc.js → core-91p43zwj.js} +2 -2
  22. package/dist/cli/{curation-policy-gs1qakn7.js → curation-policy-bqpmry2n.js} +5 -5
  23. package/dist/cli/{curator-brqack0j.js → curator-4bs2hmy9.js} +36 -36
  24. package/dist/cli/{curator-drift-1sg9yk2m.js → curator-drift-hbc3w5zj.js} +4 -4
  25. package/dist/cli/curator-llm-factory-fhbjwar0.js +70 -0
  26. package/dist/cli/{evidence-summary-service-nqbcvp30.js → evidence-summary-service-g1xd8egd.js} +16 -16
  27. package/dist/cli/{gate-evidence-9stdtpej.js → gate-evidence-gdmgsz7c.js} +7 -7
  28. package/dist/cli/guardrail-explain-1nfbtpgg.js +71 -0
  29. package/dist/cli/{guardrail-log-sf39x00b.js → guardrail-log-nyggdv04.js} +8 -8
  30. package/dist/cli/{guardrail-reset-9wr6a60s.js → guardrail-reset-rp9jq0gj.js} +36 -36
  31. package/dist/cli/{hive-promoter-bqtj8261.js → hive-promoter-39xb4tx2.js} +36 -36
  32. package/dist/cli/{index-cgewrpw5.js → index-0e9jh9c7.js} +4 -3
  33. package/dist/cli/{index-3fnym6ac.js → index-1v6vc90w.js} +4 -4
  34. package/dist/cli/{index-hg09xemc.js → index-2k53pfnc.js} +4 -4
  35. package/dist/cli/{index-wknpkfm8.js → index-3xyjvx24.js} +7 -7
  36. package/dist/cli/{index-03enxdaq.js → index-4gmt79q9.js} +1 -1
  37. package/dist/cli/{index-qgajv9rd.js → index-58be2gtd.js} +3 -3
  38. package/dist/cli/{index-r2p0pgst.js → index-5ebgzb5a.js} +3 -3
  39. package/dist/cli/{index-k0s8n6m5.js → index-5ythtd9t.js} +11 -11
  40. package/dist/cli/{index-hb7bh4r2.js → index-6be5caj8.js} +30 -6
  41. package/dist/cli/{index-yvqg6kn5.js → index-6m24v15v.js} +2 -2
  42. package/dist/cli/{index-sphahrmq.js → index-6rx1rdaq.js} +1 -1
  43. package/dist/cli/{index-r69gzes2.js → index-7h94rfem.js} +7 -7
  44. package/dist/cli/{index-xxfk62zd.js → index-8bdk7124.js} +2 -2
  45. package/dist/cli/{index-50j914b5.js → index-a0d4fp6r.js} +29 -7
  46. package/dist/cli/{index-8vw2pjgr.js → index-afpkqsdb.js} +6 -6
  47. package/dist/cli/{index-4mjmnysc.js → index-ajknebrc.js} +1 -1
  48. package/dist/cli/{index-qqq04n70.js → index-anxs69ad.js} +1 -1
  49. package/dist/cli/{index-888pq4xx.js → index-c4ddtrqg.js} +7 -7
  50. package/dist/cli/{index-ztm0q26v.js → index-dh5th4jn.js} +1 -1
  51. package/dist/cli/{index-c45662wc.js → index-e45htt92.js} +1 -1
  52. package/dist/cli/{index-ad9064j9.js → index-e6n7cemz.js} +4 -4
  53. package/dist/cli/{index-hvz4ad2t.js → index-f36hy1x4.js} +1 -1
  54. package/dist/cli/{index-40twgsyh.js → index-g8wdekzz.js} +2 -2
  55. package/dist/cli/{index-0byep4pe.js → index-gn6ybsc4.js} +8 -8
  56. package/dist/cli/{index-ewv8qa8q.js → index-gy0nyfbw.js} +2 -2
  57. package/dist/cli/{index-wasdrhev.js → index-hdakb4n9.js} +4 -4
  58. package/dist/cli/{index-5wc5pbzc.js → index-hwz46jj7.js} +682 -349
  59. package/dist/cli/{index-eed0gkv6.js → index-j2svfhzk.js} +2 -2
  60. package/dist/cli/{index-2bhz5k91.js → index-jre6tm3v.js} +2 -2
  61. package/dist/cli/{index-b25t91a3.js → index-k1ntjzmf.js} +2 -2
  62. package/dist/cli/{index-kxv193aq.js → index-n3pjcks8.js} +5 -5
  63. package/dist/cli/{index-ebce775n.js → index-p7hdp3rp.js} +2 -2
  64. package/dist/cli/{index-d3mpxsmy.js → index-ptsqnxwd.js} +7 -7
  65. package/dist/cli/{index-d7j3n5ra.js → index-qzvry1n1.js} +41 -39
  66. package/dist/cli/{index-m8xrbdmn.js → index-r5hj3nfe.js} +2 -2
  67. package/dist/cli/{index-x39hjdf1.js → index-tqw8w2n5.js} +1 -1
  68. package/dist/cli/{index-es64amqt.js → index-w6988bbc.js} +1 -1
  69. package/dist/cli/{index-mee9maf5.js → index-xwbn7c8m.js} +2 -2
  70. package/dist/cli/{index-4vb56w2k.js → index-y1zhpfk8.js} +4 -4
  71. package/dist/cli/{index-vw66q1rw.js → index-zycp24b9.js} +1 -1
  72. package/dist/cli/index.js +146 -67
  73. package/dist/cli/{knowledge-escalator-01vva51d.js → knowledge-escalator-eq07pcj3.js} +13 -13
  74. package/dist/cli/{knowledge-events-dpfwh1hs.js → knowledge-events-rv9q9mkm.js} +11 -11
  75. package/dist/cli/{knowledge-link-2rvp966j.js → knowledge-link-dgvbqjqg.js} +4 -4
  76. package/dist/cli/{knowledge-store-aw58ae7b.js → knowledge-store-cxcr0ttk.js} +5 -5
  77. package/dist/cli/{knowledge-validator-yhd31ryf.js → knowledge-validator-8qtd9ys0.js} +7 -7
  78. package/dist/cli/{model-preflight-288nsgpw.js → model-preflight-3s1c0zpc.js} +1 -1
  79. package/dist/cli/{pending-delegations-fjetcz8e.js → pending-delegations-dp3g8e7k.js} +11 -11
  80. package/dist/cli/{pr-subscriptions-cwwtfs8w.js → pr-subscriptions-3vx06rar.js} +11 -11
  81. package/dist/cli/{pr-workflow-gate-5xwtg1k2.js → pr-workflow-gate-bbet0apk.js} +36 -36
  82. package/dist/cli/{runner-mb51twxt.js → runner-0e3bjeev.js} +5 -5
  83. package/dist/cli/{scan-cursor-j11qwf0v.js → scan-cursor-zwm0xng7.js} +6 -6
  84. package/dist/cli/{schema-0xp3bde3.js → schema-ebfxgya0.js} +2 -2
  85. package/dist/cli/{scope-persistence-brrk806y.js → scope-persistence-6c55akfz.js} +15 -15
  86. package/dist/cli/{skill-generator-j8qzmet9.js → skill-generator-mv5jxzsh.js} +15 -15
  87. package/dist/cli/{snapshot-coordination-init-h0xmtftq.js → snapshot-coordination-init-n556wa18.js} +36 -36
  88. package/dist/cli/{telemetry-m84ap0pp.js → telemetry-a1yvjerj.js} +1 -1
  89. package/dist/cli/{worktree-collision-ownership-j9zykj18.js → worktree-collision-ownership-pp4z4z8p.js} +12 -12
  90. package/dist/cli/{worktree-isolation-jd39xz6h.js → worktree-isolation-69hz9dgg.js} +36 -36
  91. package/dist/commands/benchmark.d.ts +5 -1
  92. package/dist/commands/registry.d.ts +34 -43
  93. package/dist/commands/turbo.d.ts +1 -1
  94. package/dist/config/bundled-skills.d.ts +4 -3
  95. package/dist/config/constants.d.ts +13 -0
  96. package/dist/config/skill-mirrors.d.ts +3 -3
  97. package/dist/hooks/extractors.d.ts +9 -0
  98. package/dist/hooks/non-architect-advisory.d.ts +18 -0
  99. package/dist/hooks/repo-graph-injection.d.ts +16 -0
  100. package/dist/index.js +73 -71
  101. package/dist/services/decision-drift-analyzer.d.ts +7 -0
  102. package/dist/services/handoff-service.d.ts +10 -0
  103. package/dist/services/status-service.d.ts +27 -0
  104. package/dist/state.d.ts +8 -0
  105. package/dist/tools/repo-graph/builder.d.ts +1 -25
  106. package/dist/tools/repo-graph/cache.d.ts +5 -1
  107. package/dist/tools/repo-graph/fallback-scanner.d.ts +71 -0
  108. package/dist/tools/repo-graph/indexed-storage.d.ts +11 -0
  109. package/dist/tools/repo-graph/storage.d.ts +7 -0
  110. package/dist/utils/context-decisions.d.ts +77 -0
  111. package/dist/utils/sanitize-display.d.ts +7 -0
  112. package/package.json +3 -2
  113. package/dist/cli/curator-llm-factory-dtsjc3zf.js +0 -70
  114. package/dist/cli/guardrail-explain-3ff1z3fm.js +0 -71
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env bash
2
+ # repro-check.sh - disposable-worktree acceptance checks for issue-tracer v3.
3
+ set -eu
4
+ export LC_ALL=C
5
+
6
+ to_shell_path() {
7
+ case "$(uname -s 2>/dev/null || true)" in
8
+ MINGW* | MSYS* | CYGWIN*) if command -v cygpath >/dev/null 2>&1; then cygpath -u "$1"; return; fi ;;
9
+ esac
10
+ printf '%s\n' "$1"
11
+ }
12
+
13
+ root="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "repro-check: not inside a git work tree" >&2; exit 2; }
14
+ root="$(to_shell_path "$root")"
15
+ root_real="$(cd "$root" && pwd -P)"
16
+ script_dir="$(cd "$(dirname "$0")" && pwd -P)"
17
+
18
+ usage() { echo "usage: repro-check.sh {run|checkpoint|verify-checkpoint} --slug <slug> ..." >&2; exit 2; }
19
+ valid_slug() { case "$1" in ''|*[!a-z0-9-]*) return 1;; *) return 0;; esac; }
20
+ valid_id() { case "$1" in C[0-9]* ) return 0;; *) return 1;; esac; }
21
+ has_bad_field() { case "$1" in *$'\t'*|*$'\n'*|*$'\r'*) return 0;; *) return 1;; esac; }
22
+ is_inside_root() {
23
+ case "$1" in /*|[A-Za-z]:*|*\\*) return 1;; esac
24
+ case "/$1/" in */../*|*/./*) return 1;; esac
25
+ return 0
26
+ }
27
+ issue_traces_base="$root_real/.agents/issue-traces"
28
+
29
+ # Path-only preflight: trace_dir (default or --trace-dir override) must be
30
+ # textually rooted at <repo>/.agents/issue-traces/ before we touch the
31
+ # filesystem. Runs before any mkdir/write, so a caller cannot point the
32
+ # script at an arbitrary path via that flag.
33
+ validate_trace_dir_prefix() {
34
+ case "$trace_dir/" in
35
+ "$root"/.agents/issue-traces/*) ;;
36
+ *)
37
+ echo "repro-check: --trace-dir must be inside .agents/issue-traces" >&2
38
+ exit 2
39
+ ;;
40
+ esac
41
+ }
42
+
43
+ # Symlink-escape guard: resolve the deepest EXISTING ancestor of trace_dir
44
+ # (it may not exist yet) and require it to land inside the repo root. Must
45
+ # run before any mkdir -p, since mkdir -p happily follows an existing
46
+ # symlinked ancestor before any later check on the final path can catch it.
47
+ refuse_ancestor_symlink_escape() {
48
+ local check="$trace_dir" resolved
49
+ while [ ! -e "$check" ]; do check="$(dirname "$check")"; done
50
+ if [ -L "$check" ]; then
51
+ echo "repro-check: --trace-dir must be inside .agents/issue-traces" >&2
52
+ exit 2
53
+ fi
54
+ resolved="$(cd "$check" && pwd -P)"
55
+ case "$resolved/" in
56
+ "$root_real"/*) ;;
57
+ *)
58
+ echo "repro-check: --trace-dir must be inside .agents/issue-traces" >&2
59
+ exit 2
60
+ ;;
61
+ esac
62
+ }
63
+
64
+ # Post-mkdir, strict resolution check: once a path under trace_dir exists,
65
+ # its pwd -P form must land inside <repo-root>/.agents/issue-traces/. Also
66
+ # refuses the path itself being a symlink (created between the ancestor
67
+ # check above and this call).
68
+ require_contained() {
69
+ local dir="$1" resolved
70
+ [ -e "$dir" ] || return 0
71
+ if [ -L "$dir" ]; then
72
+ echo "repro-check: refusing symlinked path: $dir" >&2
73
+ exit 2
74
+ fi
75
+ resolved="$(cd "$dir" && pwd -P)"
76
+ case "$resolved/" in
77
+ "$issue_traces_base"/*) ;;
78
+ *)
79
+ echo "repro-check: refusing path outside .agents/issue-traces: $dir" >&2
80
+ exit 2
81
+ ;;
82
+ esac
83
+ }
84
+
85
+ trace_for() {
86
+ [ -n "$trace_dir" ] || trace_dir="$root/.agents/issue-traces/$slug"
87
+ validate_trace_dir_prefix
88
+ refuse_ancestor_symlink_escape
89
+ }
90
+ manifest_for() { trace_for; printf '%s\n' "$trace_dir/repro/checkpoint.manifest"; }
91
+
92
+ # Structural integrity of the checkpoint manifest, shared by `checkpoint` and
93
+ # `verify-checkpoint` so both refuse the same mangled file. Three properties:
94
+ # line 1 is the version header AND records the expected data-row count; every
95
+ # later line carries exactly 10 TAB-separated fields; the seq column counts
96
+ # 1..N with no gaps.
97
+ #
98
+ # The recorded count is what makes this total rather than a PREFIX invariant.
99
+ # Seq contiguity alone is satisfied by any prefix of a valid file, so `head -3`
100
+ # (or deleting just the last row) would still validate - silently dropping
101
+ # those checks from the replay set AND un-freezing their paths, so a plain
102
+ # `checkpoint` could then re-baseline a weakened check to green through the
103
+ # very guard below. Comparing the header count against the rows actually
104
+ # present closes the tail; seq closes the middle. Together they refuse deletion
105
+ # anywhere, reordering, duplication, and field mangling.
106
+ #
107
+ # The legacy header with no `rows=` count (`... v1`) is REJECTED rather than
108
+ # accepted for compatibility: accepting it would itself be a one-line bypass
109
+ # (write the old header, then truncate freely). Nothing is lost by failing
110
+ # closed - the manifest is an ephemeral, git-excluded, per-issue trace artifact
111
+ # seeded fresh by trace-init.sh, so there is no installed base to migrate.
112
+ #
113
+ # This is tamper-EVIDENCE, not tamper-proofing: the trace directory is the
114
+ # agent's own write surface, so a rewriter that renumbers every row AND
115
+ # restamps the header count still gets through, and so does deleting the
116
+ # manifest outright and re-running `checkpoint` (nothing binds the file's
117
+ # existence or completeness to anything outside it). What it stops is a
118
+ # partial write or a hand edit that leaves the file internally inconsistent.
119
+ validate_manifest() {
120
+ local file="$1" problem counts
121
+ problem="$(awk -F '\t' '
122
+ NR == 1 {
123
+ if ($0 !~ /^# issue-tracer checkpoint manifest v1 rows=[0-9]+$/) { bad = "header"; exit }
124
+ declared = $0
125
+ sub(/^.*rows=/, "", declared)
126
+ declared = declared + 0
127
+ next
128
+ }
129
+ {
130
+ rows += 1
131
+ if (NF != 10) { bad = "fields " NR; exit }
132
+ if ($1 "" != rows "") { bad = "seq " NR; exit }
133
+ # A path is frozen by its first row; every later row for it must be a
134
+ # reasoned AMEND. do_checkpoint refuses a duplicate CHECKPOINT at write
135
+ # time, but do_verify is last-writer-wins per path, so without this a
136
+ # forged CHECKPOINT row appended by hand (plus a bumped header count)
137
+ # would silently re-baseline a frozen blob. Checking it here makes the
138
+ # rule hold in both commands.
139
+ if (seen[$3] && $2 != "AMEND") { bad = "supersede " NR; exit }
140
+ seen[$3] = 1
141
+ }
142
+ END {
143
+ if (bad != "") { print bad; exit }
144
+ if (NR == 0) { print "header"; exit }
145
+ if (declared != rows + 0) { print "count " declared " " rows + 0 }
146
+ }
147
+ ' "$file")"
148
+ case "$problem" in
149
+ '') return 0 ;;
150
+ 'fields '*) echo "repro-check: checkpoint manifest line ${problem#fields } does not have 10 tab-separated fields" >&2 ;;
151
+ 'seq '*) echo "repro-check: checkpoint manifest seq is not contiguous (row deleted or reordered) at line ${problem#seq }" >&2 ;;
152
+ 'supersede '*) echo "repro-check: checkpoint manifest line ${problem#supersede } re-freezes an already-recorded path without an AMEND reason" >&2 ;;
153
+ 'count '*)
154
+ counts="${problem#count }"
155
+ echo "repro-check: checkpoint manifest header records ${counts%% *} rows, found ${counts#* } (rows deleted or truncated)" >&2
156
+ ;;
157
+ *) echo "repro-check: invalid manifest header (want '# issue-tracer checkpoint manifest v1 rows=<N>' on line 1)" >&2 ;;
158
+ esac
159
+ exit 2
160
+ }
161
+
162
+ # True when field 3 (the path column) of any data row already names this path.
163
+ # A recorded path is frozen: only a reasoned AMEND may supersede it.
164
+ manifest_has_path() {
165
+ awk -F '\t' -v want="$2" 'NR > 1 && $3 == want { found = 1; exit } END { exit !found }' "$1"
166
+ }
167
+
168
+ # Append one data row and restamp the header's recorded count in a single
169
+ # temp-file swap, so the manifest is never observable in a state where the
170
+ # count and the rows disagree - a mid-loop `exit` (an invalid later path, an
171
+ # already-frozen repeat) leaves a consistent file rather than bricking it.
172
+ # Mirrors bound_log's `$log.truncate.tmp` pattern; refuse_nonregular_target
173
+ # guards the temp name as well as the manifest, and `mv` renames over the
174
+ # destination rather than writing through a symlink planted there.
175
+ append_manifest_row() {
176
+ local file="$1" count="$2" row="$3" temp
177
+ temp="$file.append.tmp"
178
+ refuse_nonregular_target "$file"
179
+ refuse_nonregular_target "$temp"
180
+ {
181
+ printf '# issue-tracer checkpoint manifest v1 rows=%s\n' "$count"
182
+ awk 'NR > 1' "$file"
183
+ printf '%s\n' "$row"
184
+ } > "$temp"
185
+ mv "$temp" "$file"
186
+ }
187
+
188
+ # Refuse to redirect/append into a pre-existing path that is not a regular
189
+ # file (a symlink, device, fifo, directory, ...). Must run immediately before
190
+ # every `>`/`>>` into a leaf artifact path (base/head logs, checkpoint
191
+ # manifest), since a pre-existing symlink there would otherwise be silently
192
+ # followed by the shell redirection, writing through it to an arbitrary
193
+ # target.
194
+ refuse_nonregular_target() {
195
+ local path="$1"
196
+ if { [ -e "$path" ] && [ ! -f "$path" ]; } || [ -L "$path" ]; then
197
+ echo "repro-check: refusing non-regular target: $path" >&2
198
+ exit 2
199
+ fi
200
+ }
201
+
202
+ bound_log() {
203
+ local log="$1" total kept=1048576 omitted temp
204
+ total="$(wc -c < "$log" | tr -d ' ')"
205
+ [ "$total" -le 2097152 ] && return
206
+ temp="$log.truncate.tmp"
207
+ refuse_nonregular_target "$temp"
208
+ { head -c "$kept" "$log"; printf '\n[... truncated %s bytes ...]\n' "$((total - (kept * 2)))"; tail -c "$kept" "$log"; } > "$temp"
209
+ mv "$temp" "$log"
210
+ }
211
+
212
+ run_one() {
213
+ local cwd="$1" log="$2" seconds="$3"; shift 3
214
+ local status=0 pid started timed=0 waited=0
215
+ refuse_nonregular_target "$log"
216
+ : > "$log"
217
+ if [ "${REPRO_CHECK_FORCE_FALLBACK:-0}" != "1" ] && command -v timeout >/dev/null 2>&1; then
218
+ ( cd "$cwd" && timeout --foreground -k 5 "${seconds}s" "$@" ) > "$log" 2>&1 || status=$?
219
+ else
220
+ # POSIX watchdog fallback: run the child in its own process group so the
221
+ # whole group (not just the immediate child) can be killed on timeout.
222
+ if command -v setsid >/dev/null 2>&1; then
223
+ ( cd "$cwd" && exec setsid "$@" ) > "$log" 2>&1 &
224
+ else
225
+ set -m
226
+ ( cd "$cwd" && "$@" ) > "$log" 2>&1 &
227
+ set +m
228
+ fi
229
+ pid=$!
230
+ started=$SECONDS
231
+ while kill -0 "$pid" 2>/dev/null; do
232
+ if [ "$((SECONDS - started))" -ge "$seconds" ]; then
233
+ timed=1
234
+ kill -TERM -- "-$pid" >/dev/null 2>&1 || kill -TERM "$pid" >/dev/null 2>&1 || true
235
+ waited=0
236
+ while [ "$waited" -lt 5 ] && kill -0 "$pid" 2>/dev/null; do sleep 1; waited=$((waited + 1)); done
237
+ kill -KILL -- "-$pid" >/dev/null 2>&1 || kill -KILL "$pid" >/dev/null 2>&1 || true
238
+ break
239
+ fi
240
+ sleep 1
241
+ done
242
+ wait "$pid" 2>/dev/null || status=$?
243
+ [ "$timed" -eq 0 ] || status=124
244
+ fi
245
+ bound_log "$log"
246
+ printf '%s\n' "$status"
247
+ }
248
+
249
+ copy_path() {
250
+ local rel="$1" source target source_dir
251
+ is_inside_root "$rel" || { echo "repro-check: --copy path must be repo-relative without ..: $rel" >&2; exit 2; }
252
+ source="$root/$rel"
253
+ [ -e "$source" ] || { echo "repro-check: --copy path does not exist: $rel" >&2; exit 2; }
254
+ source_dir="$(cd "$(dirname "$source")" && pwd -P)"
255
+ case "$source_dir" in "$root_real"|"$root_real"/*) ;; *) echo "repro-check: --copy path resolves outside repo: $rel" >&2; exit 2;; esac
256
+ target="$worktree/$rel"
257
+ mkdir -p "$(dirname "$target")"
258
+ rm -rf "$target"
259
+ cp -R "$source" "$target"
260
+ }
261
+
262
+ link_deps() {
263
+ [ "$deps" = link ] || return 0
264
+ [ -e "$root/node_modules" ] || return 0
265
+ [ ! -e "$worktree/node_modules" ] || return 0
266
+ case "$(uname -s 2>/dev/null || true)" in
267
+ MINGW* | MSYS* | CYGWIN*) cmd //c mklink //J "$(to_shell_path "$worktree/node_modules")" "$(to_shell_path "$root/node_modules")" >/dev/null 2>&1 || true ;;
268
+ *) ln -s "$root/node_modules" "$worktree/node_modules" ;;
269
+ esac
270
+ }
271
+
272
+ quoted_argv() { local arg; for arg in "$@"; do printf '%q ' "$arg"; done; }
273
+
274
+ do_run() {
275
+ local base="" class="" check_id="" expect="" timeout_seconds=600 deps=link arg base_status head_status base_result head_result verdict exit_code=0
276
+ local copies=()
277
+ trace_dir=""; slug=""
278
+ while [ "$#" -gt 0 ]; do
279
+ case "$1" in
280
+ --base) [ "$#" -ge 2 ] || usage; base="$2"; shift 2;;
281
+ --class) [ "$#" -ge 2 ] || usage; class="$2"; shift 2;;
282
+ --id) [ "$#" -ge 2 ] || usage; check_id="$2"; shift 2;;
283
+ --expect) [ "$#" -ge 2 ] || usage; expect="$2"; shift 2;;
284
+ --copy) [ "$#" -ge 2 ] || usage; copies+=("$2"); shift 2;;
285
+ --deps) [ "$#" -ge 2 ] || usage; deps="$2"; shift 2;;
286
+ --timeout) [ "$#" -ge 2 ] || usage; timeout_seconds="$2"; shift 2;;
287
+ --slug) [ "$#" -ge 2 ] || usage; slug="$2"; shift 2;;
288
+ --trace-dir) [ "$#" -ge 2 ] || usage; trace_dir="$2"; shift 2;;
289
+ --) shift; break;;
290
+ *) usage;;
291
+ esac
292
+ done
293
+ [ "$#" -gt 0 ] || usage
294
+ valid_slug "$slug" && valid_id "$check_id" || { echo "repro-check: invalid slug or check id" >&2; exit 2; }
295
+ case "$base" in ''|-*) echo "repro-check: --base must name a commit" >&2; exit 2;; esac
296
+ git rev-parse --verify --quiet "$base^{commit}" >/dev/null || { echo "repro-check: --base does not resolve to a commit" >&2; exit 2; }
297
+ case "$class" in DISCRIMINATING|PRESERVING|NEW-SURFACE) ;; *) echo "repro-check: unknown class" >&2; exit 2;; esac
298
+ case "$deps" in link|none) ;; *) echo "repro-check: --deps must be link or none" >&2; exit 2;; esac
299
+ case "$timeout_seconds" in ''|*[!0-9]*|0) echo "repro-check: --timeout must be a positive integer" >&2; exit 2;; esac
300
+ case "$class" in DISCRIMINATING|NEW-SURFACE) [ -n "$expect" ] || { echo "repro-check: --expect is required for $class" >&2; exit 2; };; esac
301
+ has_bad_field "$expect" && { echo "repro-check: --expect cannot contain tabs or newlines" >&2; exit 2; }
302
+ trace_for
303
+ require_contained "$trace_dir"
304
+ require_contained "$trace_dir/repro"
305
+ mkdir -p "$trace_dir/repro"
306
+ require_contained "$trace_dir/repro"
307
+ worktree="$(mktemp -d "${TMPDIR:-/tmp}/issue-tracer-repro.XXXXXX")"
308
+ cleanup() { git worktree remove --force "$worktree" >/dev/null 2>&1 || true; rm -rf "$worktree"; }
309
+ trap cleanup EXIT HUP INT TERM
310
+ git worktree add --detach "$worktree" "$base" >/dev/null 2>&1 || { echo "repro-check: could not create disposable worktree" >&2; exit 2; }
311
+ for arg in ${copies[@]+"${copies[@]}"}; do copy_path "$arg"; done
312
+ link_deps
313
+ base_status="$(run_one "$worktree" "$trace_dir/repro/$check_id.base.log" "$timeout_seconds" "$@")"
314
+ head_status="$(run_one "$root" "$trace_dir/repro/$check_id.head.log" "$timeout_seconds" "$@")"
315
+ if [ "$base_status" -eq 124 ] || [ "$head_status" -eq 124 ]; then
316
+ base_result="TIMEOUT"; head_result="TIMEOUT"; verdict="FAIL"; exit_code=6
317
+ elif [ "$class" = DISCRIMINATING ]; then
318
+ if [ "$base_status" -eq 0 ]; then base_result="VACUOUS"; verdict="VACUOUS"; exit_code=4
319
+ elif grep -Eq "$expect" "$trace_dir/repro/$check_id.base.log"; then
320
+ base_result="RED"
321
+ if [ "$head_status" -eq 0 ]; then head_result="GREEN"; verdict="PASS"; else head_result="FAIL"; verdict="FAIL"; exit_code=5; fi
322
+ else base_result="ERROR"; verdict="ERROR"; exit_code=3; fi
323
+ elif [ "$class" = PRESERVING ]; then
324
+ base_result="$( [ "$base_status" -eq 0 ] && echo GREEN || echo FAIL )"
325
+ head_result="$( [ "$head_status" -eq 0 ] && echo GREEN || echo FAIL )"
326
+ if [ "$base_status" -eq 0 ] && [ "$head_status" -eq 0 ]; then verdict=PASS; else verdict=FAIL; exit_code=5; fi
327
+ else
328
+ if [ "$base_status" -ne 0 ] && grep -Eq "$expect" "$trace_dir/repro/$check_id.base.log"; then
329
+ base_result="ERROR"
330
+ if [ "$head_status" -eq 0 ]; then head_result="GREEN"; verdict=PASS; else head_result="FAIL"; verdict=FAIL; exit_code=5; fi
331
+ else base_result="FAIL"; head_result="$( [ "$head_status" -eq 0 ] && echo GREEN || echo FAIL )"; verdict=FAIL; exit_code=5; fi
332
+ fi
333
+ [ -n "${head_result:-}" ] || head_result="$( [ "$head_status" -eq 0 ] && echo GREEN || echo FAIL )"
334
+ printf '### Check %s (%s)\n- base: %s exit=%s result=%s log=repro/%s.base.log\n- head: %s exit=%s result=%s log=repro/%s.head.log\n- argv: %s\n- expect: %s\n- verdict: %s\n' "$check_id" "$class" "$base" "$base_status" "$base_result" "$check_id" "$(git rev-parse HEAD)" "$head_status" "$head_result" "$check_id" "$(quoted_argv "$@")" "${expect:--}" "$verdict"
335
+ exit "$exit_code"
336
+ }
337
+
338
+ do_checkpoint() {
339
+ local reason="-" check_id="" argv="" expect="" base="" path manifest seq kind mode blob row
340
+ trace_dir=""; slug=""
341
+ while [ "$#" -gt 0 ]; do
342
+ case "$1" in
343
+ --slug) [ "$#" -ge 2 ] || usage; slug="$2"; shift 2;; --trace-dir) [ "$#" -ge 2 ] || usage; trace_dir="$2"; shift 2;;
344
+ --reason) [ "$#" -ge 2 ] || usage; reason="$2"; shift 2;; --id) [ "$#" -ge 2 ] || usage; check_id="$2"; shift 2;;
345
+ --argv) [ "$#" -ge 2 ] || usage; argv="$2"; shift 2;; --expect) [ "$#" -ge 2 ] || usage; expect="$2"; shift 2;; --base) [ "$#" -ge 2 ] || usage; base="$2"; shift 2;;
346
+ --) shift; break;; *) break;;
347
+ esac
348
+ done
349
+ [ "$#" -gt 0 ] || usage
350
+ valid_slug "$slug" && valid_id "$check_id" || { echo "repro-check: invalid slug or check id" >&2; exit 2; }
351
+ case "$reason" in -) kind=CHECKPOINT;; CHECK_WRONG|FORMAT_ONLY|AC_CHANGED_BY_USER) kind=AMEND;; *) echo "repro-check: invalid amendment reason" >&2; exit 2;; esac
352
+ case "$base" in ''|-*) echo "repro-check: --base must name a commit" >&2; exit 2;; esac
353
+ git rev-parse --verify --quiet "$base^{commit}" >/dev/null || { echo "repro-check: --base does not resolve to a commit" >&2; exit 2; }
354
+ if has_bad_field "$argv" || has_bad_field "$expect"; then
355
+ echo "repro-check: manifest fields cannot contain tabs or newlines" >&2
356
+ exit 2
357
+ fi
358
+ manifest="$(manifest_for)"
359
+ require_contained "$trace_dir"
360
+ require_contained "$(dirname "$manifest")"
361
+ mkdir -p "$(dirname "$manifest")"
362
+ require_contained "$(dirname "$manifest")"
363
+ refuse_nonregular_target "$manifest"
364
+ [ -f "$manifest" ] || printf '# issue-tracer checkpoint manifest v1 rows=0\n' > "$manifest"
365
+ # validate_manifest owns the header check: it is strictly stronger than the
366
+ # old `grep -Fx` (which matched the string on ANY line) and additionally
367
+ # proves the recorded count, the seq run, and the field count.
368
+ validate_manifest "$manifest"
369
+ seq="$(awk 'END {print NR - 1}' "$manifest")"
370
+ for path in "$@"; do
371
+ is_inside_root "$path" || { echo "repro-check: checkpoint path must be repo-relative without ..: $path" >&2; exit 2; }
372
+ has_bad_field "$path" && { echo "repro-check: checkpoint path cannot contain tabs or newlines" >&2; exit 2; }
373
+ [ -f "$root/$path" ] || { echo "repro-check: checkpoint path must be a file: $path" >&2; exit 2; }
374
+ # Re-running the sanctioned `checkpoint` command on an already-frozen path
375
+ # would append a fresh CHECKPOINT row that last-writer-wins re-baselines a
376
+ # weakened check to green in do_verify. Refuse it: superseding a frozen
377
+ # blob requires an AMEND row that names a reason and stays in the file.
378
+ # The manifest is re-read per path, so a path frozen by an earlier
379
+ # iteration of this same invocation is already recorded and also refused.
380
+ if [ "$kind" = CHECKPOINT ] && manifest_has_path "$manifest" "$path"; then
381
+ echo "repro-check: $path is already frozen; supersede it with --reason CHECK_WRONG|FORMAT_ONLY|AC_CHANGED_BY_USER" >&2
382
+ exit 2
383
+ fi
384
+ blob="$(git hash-object "$root/$path")"
385
+ mode="$(git ls-files -s -- "$path" | awk 'NR==1 {print $1}')"
386
+ [ -n "$mode" ] || { [ -x "$root/$path" ] && mode=100755 || mode=100644; }
387
+ seq=$((seq + 1))
388
+ # $seq is post-increment, so it is also the new total row count that the
389
+ # header must record. Row and header land together in one temp-file swap.
390
+ row="$(printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s' "$seq" "$kind" "$path" "$blob" "$mode" "$check_id" "$argv" "$expect" "$(git rev-parse "$base^{commit}")" "$reason")"
391
+ append_manifest_row "$manifest" "$seq" "$row"
392
+ echo "checkpoint: $kind $path"
393
+ done
394
+ }
395
+
396
+ do_verify() {
397
+ local manifest line path old new changed=0
398
+ trace_dir=""; slug=""
399
+ while [ "$#" -gt 0 ]; do case "$1" in --slug) [ "$#" -ge 2 ] || usage; slug="$2"; shift 2;; --trace-dir) [ "$#" -ge 2 ] || usage; trace_dir="$2"; shift 2;; *) usage;; esac; done
400
+ valid_slug "$slug" || { echo "repro-check: invalid slug" >&2; exit 2; }
401
+ manifest="$(manifest_for)"
402
+ [ -f "$manifest" ] || { echo "repro-check: checkpoint manifest missing or invalid" >&2; exit 2; }
403
+ # Iterating only the surviving rows would silently drop a frozen check when a
404
+ # row is deleted OR the tail is truncated, so structure - header count, seq
405
+ # run, field count - is proven before any row is replayed.
406
+ validate_manifest "$manifest"
407
+ while IFS=$'\t' read -r path old; do
408
+ [ -f "$root/$path" ] || { echo "CHANGED $path $old MISSING"; changed=1; continue; }
409
+ new="$(git hash-object "$root/$path")"
410
+ if [ "$old" = "$new" ]; then echo "OK $path"; else echo "CHANGED $path $old $new"; changed=1; fi
411
+ done < <(awk -F '\t' 'NR > 1 {blob[$3]=$4} END {for (path in blob) print path "\t" blob[path]}' "$manifest")
412
+ [ "$changed" -eq 0 ] || exit 1
413
+ }
414
+
415
+ command="${1:-}"; shift || true
416
+ case "$command" in run) do_run "$@";; checkpoint) do_checkpoint "$@";; verify-checkpoint) do_verify "$@";; *) usage;; esac