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.
- package/.opencode/skills/durable-session-state/SKILL.md +1 -1
- package/.opencode/skills/issue-tracer/SKILL.md +140 -286
- package/.opencode/skills/issue-tracer/assets/pr-template.md +13 -7
- package/.opencode/skills/issue-tracer/references/acceptance-checks.md +66 -0
- package/.opencode/skills/issue-tracer/references/critic-gate.md +53 -11
- package/.opencode/skills/issue-tracer/references/evidence-artifacts.md +128 -23
- package/.opencode/skills/issue-tracer/references/full-resolution-contract.md +69 -0
- package/.opencode/skills/issue-tracer/references/install.md +31 -9
- package/.opencode/skills/issue-tracer/references/localization-playbook.md +26 -14
- package/.opencode/skills/issue-tracer/references/method-provenance.md +19 -3
- package/.opencode/skills/issue-tracer/references/phase-0-setup.md +90 -0
- package/.opencode/skills/issue-tracer/references/phase-1-intake.md +43 -0
- package/.opencode/skills/issue-tracer/references/untrusted-content.md +13 -5
- package/.opencode/skills/issue-tracer/scripts/repro-check.sh +416 -0
- package/.opencode/skills/issue-tracer/scripts/trace-check.sh +479 -0
- package/.opencode/skills/issue-tracer/scripts/trace-init.sh +96 -6
- package/.opencode/skills/loop/SKILL.md +1 -1
- package/.opencode/skills/{plan → swarm-plan}/SKILL.md +1 -1
- package/dist/cli/{coder-settlement-yh1q5fr3.js → coder-settlement-89nt4j7g.js} +11 -11
- package/dist/cli/{config-doctor-bd4yzmh7.js → config-doctor-gm6f5chm.js} +3 -3
- package/dist/cli/{core-y0s1vmyc.js → core-91p43zwj.js} +2 -2
- package/dist/cli/{curation-policy-gs1qakn7.js → curation-policy-bqpmry2n.js} +5 -5
- package/dist/cli/{curator-brqack0j.js → curator-4bs2hmy9.js} +36 -36
- package/dist/cli/{curator-drift-1sg9yk2m.js → curator-drift-hbc3w5zj.js} +4 -4
- package/dist/cli/curator-llm-factory-fhbjwar0.js +70 -0
- package/dist/cli/{evidence-summary-service-nqbcvp30.js → evidence-summary-service-g1xd8egd.js} +16 -16
- package/dist/cli/{gate-evidence-9stdtpej.js → gate-evidence-gdmgsz7c.js} +7 -7
- package/dist/cli/guardrail-explain-1nfbtpgg.js +71 -0
- package/dist/cli/{guardrail-log-sf39x00b.js → guardrail-log-nyggdv04.js} +8 -8
- package/dist/cli/{guardrail-reset-9wr6a60s.js → guardrail-reset-rp9jq0gj.js} +36 -36
- package/dist/cli/{hive-promoter-bqtj8261.js → hive-promoter-39xb4tx2.js} +36 -36
- package/dist/cli/{index-cgewrpw5.js → index-0e9jh9c7.js} +4 -3
- package/dist/cli/{index-3fnym6ac.js → index-1v6vc90w.js} +4 -4
- package/dist/cli/{index-hg09xemc.js → index-2k53pfnc.js} +4 -4
- package/dist/cli/{index-wknpkfm8.js → index-3xyjvx24.js} +7 -7
- package/dist/cli/{index-03enxdaq.js → index-4gmt79q9.js} +1 -1
- package/dist/cli/{index-qgajv9rd.js → index-58be2gtd.js} +3 -3
- package/dist/cli/{index-r2p0pgst.js → index-5ebgzb5a.js} +3 -3
- package/dist/cli/{index-k0s8n6m5.js → index-5ythtd9t.js} +11 -11
- package/dist/cli/{index-hb7bh4r2.js → index-6be5caj8.js} +30 -6
- package/dist/cli/{index-yvqg6kn5.js → index-6m24v15v.js} +2 -2
- package/dist/cli/{index-sphahrmq.js → index-6rx1rdaq.js} +1 -1
- package/dist/cli/{index-r69gzes2.js → index-7h94rfem.js} +7 -7
- package/dist/cli/{index-xxfk62zd.js → index-8bdk7124.js} +2 -2
- package/dist/cli/{index-50j914b5.js → index-a0d4fp6r.js} +29 -7
- package/dist/cli/{index-8vw2pjgr.js → index-afpkqsdb.js} +6 -6
- package/dist/cli/{index-4mjmnysc.js → index-ajknebrc.js} +1 -1
- package/dist/cli/{index-qqq04n70.js → index-anxs69ad.js} +1 -1
- package/dist/cli/{index-888pq4xx.js → index-c4ddtrqg.js} +7 -7
- package/dist/cli/{index-ztm0q26v.js → index-dh5th4jn.js} +1 -1
- package/dist/cli/{index-c45662wc.js → index-e45htt92.js} +1 -1
- package/dist/cli/{index-ad9064j9.js → index-e6n7cemz.js} +4 -4
- package/dist/cli/{index-hvz4ad2t.js → index-f36hy1x4.js} +1 -1
- package/dist/cli/{index-40twgsyh.js → index-g8wdekzz.js} +2 -2
- package/dist/cli/{index-0byep4pe.js → index-gn6ybsc4.js} +8 -8
- package/dist/cli/{index-ewv8qa8q.js → index-gy0nyfbw.js} +2 -2
- package/dist/cli/{index-wasdrhev.js → index-hdakb4n9.js} +4 -4
- package/dist/cli/{index-5wc5pbzc.js → index-hwz46jj7.js} +682 -349
- package/dist/cli/{index-eed0gkv6.js → index-j2svfhzk.js} +2 -2
- package/dist/cli/{index-2bhz5k91.js → index-jre6tm3v.js} +2 -2
- package/dist/cli/{index-b25t91a3.js → index-k1ntjzmf.js} +2 -2
- package/dist/cli/{index-kxv193aq.js → index-n3pjcks8.js} +5 -5
- package/dist/cli/{index-ebce775n.js → index-p7hdp3rp.js} +2 -2
- package/dist/cli/{index-d3mpxsmy.js → index-ptsqnxwd.js} +7 -7
- package/dist/cli/{index-d7j3n5ra.js → index-qzvry1n1.js} +41 -39
- package/dist/cli/{index-m8xrbdmn.js → index-r5hj3nfe.js} +2 -2
- package/dist/cli/{index-x39hjdf1.js → index-tqw8w2n5.js} +1 -1
- package/dist/cli/{index-es64amqt.js → index-w6988bbc.js} +1 -1
- package/dist/cli/{index-mee9maf5.js → index-xwbn7c8m.js} +2 -2
- package/dist/cli/{index-4vb56w2k.js → index-y1zhpfk8.js} +4 -4
- package/dist/cli/{index-vw66q1rw.js → index-zycp24b9.js} +1 -1
- package/dist/cli/index.js +146 -67
- package/dist/cli/{knowledge-escalator-01vva51d.js → knowledge-escalator-eq07pcj3.js} +13 -13
- package/dist/cli/{knowledge-events-dpfwh1hs.js → knowledge-events-rv9q9mkm.js} +11 -11
- package/dist/cli/{knowledge-link-2rvp966j.js → knowledge-link-dgvbqjqg.js} +4 -4
- package/dist/cli/{knowledge-store-aw58ae7b.js → knowledge-store-cxcr0ttk.js} +5 -5
- package/dist/cli/{knowledge-validator-yhd31ryf.js → knowledge-validator-8qtd9ys0.js} +7 -7
- package/dist/cli/{model-preflight-288nsgpw.js → model-preflight-3s1c0zpc.js} +1 -1
- package/dist/cli/{pending-delegations-fjetcz8e.js → pending-delegations-dp3g8e7k.js} +11 -11
- package/dist/cli/{pr-subscriptions-cwwtfs8w.js → pr-subscriptions-3vx06rar.js} +11 -11
- package/dist/cli/{pr-workflow-gate-5xwtg1k2.js → pr-workflow-gate-bbet0apk.js} +36 -36
- package/dist/cli/{runner-mb51twxt.js → runner-0e3bjeev.js} +5 -5
- package/dist/cli/{scan-cursor-j11qwf0v.js → scan-cursor-zwm0xng7.js} +6 -6
- package/dist/cli/{schema-0xp3bde3.js → schema-ebfxgya0.js} +2 -2
- package/dist/cli/{scope-persistence-brrk806y.js → scope-persistence-6c55akfz.js} +15 -15
- package/dist/cli/{skill-generator-j8qzmet9.js → skill-generator-mv5jxzsh.js} +15 -15
- package/dist/cli/{snapshot-coordination-init-h0xmtftq.js → snapshot-coordination-init-n556wa18.js} +36 -36
- package/dist/cli/{telemetry-m84ap0pp.js → telemetry-a1yvjerj.js} +1 -1
- package/dist/cli/{worktree-collision-ownership-j9zykj18.js → worktree-collision-ownership-pp4z4z8p.js} +12 -12
- package/dist/cli/{worktree-isolation-jd39xz6h.js → worktree-isolation-69hz9dgg.js} +36 -36
- package/dist/commands/benchmark.d.ts +5 -1
- package/dist/commands/registry.d.ts +34 -43
- package/dist/commands/turbo.d.ts +1 -1
- package/dist/config/bundled-skills.d.ts +4 -3
- package/dist/config/constants.d.ts +13 -0
- package/dist/config/skill-mirrors.d.ts +3 -3
- package/dist/hooks/extractors.d.ts +9 -0
- package/dist/hooks/non-architect-advisory.d.ts +18 -0
- package/dist/hooks/repo-graph-injection.d.ts +16 -0
- package/dist/index.js +73 -71
- package/dist/services/decision-drift-analyzer.d.ts +7 -0
- package/dist/services/handoff-service.d.ts +10 -0
- package/dist/services/status-service.d.ts +27 -0
- package/dist/state.d.ts +8 -0
- package/dist/tools/repo-graph/builder.d.ts +1 -25
- package/dist/tools/repo-graph/cache.d.ts +5 -1
- package/dist/tools/repo-graph/fallback-scanner.d.ts +71 -0
- package/dist/tools/repo-graph/indexed-storage.d.ts +11 -0
- package/dist/tools/repo-graph/storage.d.ts +7 -0
- package/dist/utils/context-decisions.d.ts +77 -0
- package/dist/utils/sanitize-display.d.ts +7 -0
- package/package.json +3 -2
- package/dist/cli/curator-llm-factory-dtsjc3zf.js +0 -70
- 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
|