opencode-swarm 7.184.5 → 7.184.7
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/issue-tracer/SKILL.md +3 -3
- package/.opencode/skills/issue-tracer/assets/pr-template.md +7 -0
- package/.opencode/skills/issue-tracer/references/acceptance-checks.md +30 -6
- package/.opencode/skills/issue-tracer/references/critic-gate.md +5 -0
- package/.opencode/skills/issue-tracer/references/evidence-artifacts.md +18 -2
- package/.opencode/skills/issue-tracer/scripts/repro-check.sh +604 -77
- package/.opencode/skills/issue-tracer/scripts/trace-check.sh +486 -60
- package/.opencode/skills/issue-tracer/scripts/trace-init.sh +30 -0
- package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
- package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
- package/dist/cli/{coder-settlement-m3r1kmzx.js → coder-settlement-2759zt77.js} +11 -11
- package/dist/cli/{config-doctor-1qgq2s96.js → config-doctor-g48tz4kt.js} +3 -3
- package/dist/cli/{core-h7pp6xzs.js → core-1jfgxw72.js} +4 -4
- package/dist/cli/{curation-policy-49f8jx48.js → curation-policy-k43q6b91.js} +4 -4
- package/dist/cli/{curator-drift-epvr1y5j.js → curator-drift-5wc9jfmy.js} +9 -9
- package/dist/cli/curator-llm-factory-jjd0cxb2.js +75 -0
- package/dist/cli/{curator-48mky2w8.js → curator-m7sja3jp.js} +38 -38
- package/dist/cli/{evidence-summary-service-akzas6he.js → evidence-summary-service-sx8x12w0.js} +17 -17
- package/dist/cli/{gate-evidence-gmq1th1b.js → gate-evidence-znfffz94.js} +8 -6
- package/dist/cli/guardrail-explain-x81m4s8q.js +76 -0
- package/dist/cli/{guardrail-log-n2cgvbh1.js → guardrail-log-mcv4ad3t.js} +7 -7
- package/dist/cli/{guardrail-reset-3br6fs4n.js → guardrail-reset-ksrr2r0n.js} +38 -38
- package/dist/cli/{hive-promoter-a1yw6evd.js → hive-promoter-06dzq5ms.js} +38 -38
- package/dist/cli/{index-fnbcd9ve.js → index-0prgjynb.js} +5 -5
- package/dist/cli/{index-ar2gsn6b.js → index-14j1jkyy.js} +6 -6
- package/dist/cli/{index-2zzmthnv.js → index-16smfppa.js} +4 -4
- package/dist/cli/{index-k1cxzdbd.js → index-1kz2repk.js} +4 -4
- package/dist/cli/{index-7ge1gcd3.js → index-1npynszx.js} +12 -12
- package/dist/cli/{index-82hy3v8f.js → index-1nwkzj24.js} +2 -2
- package/dist/cli/{index-ya32wfkp.js → index-1t2ctwkd.js} +1 -1
- package/dist/cli/{index-8pfcj7tz.js → index-2s0gy81b.js} +3 -3
- package/dist/cli/{index-k53a69vb.js → index-2t4zv1ag.js} +7 -14
- package/dist/cli/{index-3ny2d85s.js → index-3h0f07e9.js} +2 -2
- package/dist/cli/{index-qf75z3xz.js → index-42pchr9f.js} +1 -1
- package/dist/cli/{index-6jqxstq4.js → index-4qmeshfa.js} +5 -5
- package/dist/cli/{index-3n5zsp31.js → index-7xv2hvr6.js} +2 -2
- package/dist/cli/{index-10rsjhfy.js → index-82j1een1.js} +46 -19
- package/dist/cli/{index-h9hvszf7.js → index-8bvpcbk8.js} +40 -40
- package/dist/cli/{index-wy3zpwjy.js → index-98a0rxby.js} +1 -1
- package/dist/cli/{index-vyjped9g.js → index-b1v0zxnh.js} +2 -2
- package/dist/cli/{index-cdvb33mn.js → index-bf055zyw.js} +2 -2
- package/dist/cli/{index-2adz24wr.js → index-c5c7hrz1.js} +3 -3
- package/dist/cli/{index-6bn74qs9.js → index-dyddcx8h.js} +168 -105
- package/dist/cli/{index-xf69xty9.js → index-e1zrgvqg.js} +1 -1
- package/dist/cli/{index-yqcaf6r3.js → index-eecz2kna.js} +7 -7
- package/dist/cli/{index-1511m10x.js → index-enjsn68b.js} +7 -7
- package/dist/cli/{index-wzec3c4g.js → index-ezagem2g.js} +4 -0
- package/dist/cli/{index-zzvxj02e.js → index-fr6x6vn2.js} +1 -1
- package/dist/cli/{index-475bkab8.js → index-g2r6t7by.js} +1 -1
- package/dist/cli/{index-tzejdtyx.js → index-kbd6apth.js} +38 -38
- package/dist/cli/{index-e602am4j.js → index-m8rt7a3g.js} +4 -4
- package/dist/cli/{index-xf6j4ypk.js → index-mp60wy1f.js} +4 -4
- package/dist/cli/{index-6g664sy8.js → index-q39ecyaq.js} +1 -1
- package/dist/cli/{index-tajh3egk.js → index-q4a0j3nh.js} +5 -5
- package/dist/cli/{index-earq3mda.js → index-qeqn241m.js} +1 -1
- package/dist/cli/{index-ae2jf510.js → index-raj6y6d3.js} +2 -2
- package/dist/cli/{index-f0nj3d8z.js → index-rmspt7xm.js} +1 -1
- package/dist/cli/{index-ezpzd88b.js → index-rwzrbcm9.js} +2 -2
- package/dist/cli/{index-jvk95e6y.js → index-s95ghsy1.js} +1 -1
- package/dist/cli/{index-f4z1ez09.js → index-sxxj47cy.js} +6 -6
- package/dist/cli/{index-w9hxzcz4.js → index-t0mb5a9p.js} +1 -1
- package/dist/cli/{index-wehwbv89.js → index-x16a0c6b.js} +7 -7
- package/dist/cli/{index-456awy1p.js → index-xqxqepsy.js} +2 -2
- package/dist/cli/{index-zeg8y1fw.js → index-ywmknn6m.js} +1 -1
- package/dist/cli/{index-d1bfyxj5.js → index-z1qs160n.js} +1 -1
- package/dist/cli/{index-gcdgtcg5.js → index-z7recmq5.js} +3 -3
- package/dist/cli/index.d.ts +36 -7
- package/dist/cli/index.js +352 -92
- package/dist/cli/{knowledge-escalator-rdgspj3v.js → knowledge-escalator-ghx76dcv.js} +12 -12
- package/dist/cli/{knowledge-events-zh16e78y.js → knowledge-events-kpw6fbmc.js} +10 -10
- package/dist/cli/{knowledge-link-ap2274vm.js → knowledge-link-xw6vsbx3.js} +3 -3
- package/dist/cli/{knowledge-store-jxke13cv.js → knowledge-store-fhdrpxdm.js} +4 -4
- package/dist/cli/{knowledge-validator-24myt9fp.js → knowledge-validator-7jr0aka1.js} +6 -6
- package/dist/cli/{mcp-a4q99fmb.js → mcp-v7cgf5tn.js} +1 -1
- package/dist/cli/{model-preflight-qy88p9fc.js → model-preflight-kn3xqvfk.js} +38 -38
- package/dist/cli/{pending-delegations-9vqyq3bj.js → pending-delegations-z7mcbecn.js} +10 -10
- package/dist/cli/{pr-subscriptions-hrefmtay.js → pr-subscriptions-1sb07tx3.js} +10 -10
- package/dist/cli/{pr-workflow-gate-1jcs4wnp.js → pr-workflow-gate-ph4fvf47.js} +38 -38
- package/dist/cli/{runner-yz3ysehf.js → runner-pvdhxh74.js} +4 -4
- package/dist/cli/{scan-cursor-krn56ew2.js → scan-cursor-7ny9bnmk.js} +5 -5
- package/dist/cli/{schema-zqankczb.js → schema-pjkzvcw8.js} +2 -2
- package/dist/cli/{scope-persistence-5qrjm5d2.js → scope-persistence-mkb8yc5s.js} +14 -14
- package/dist/cli/{server-7wydssat.js → server-4e853cwk.js} +38 -38
- package/dist/cli/{skill-generator-br4wctes.js → skill-generator-v6n0kwh0.js} +14 -14
- package/dist/cli/{snapshot-coordination-init-g4sg2fxv.js → snapshot-coordination-init-agx2sn6h.js} +38 -38
- package/dist/cli/{speckit-checkoff-82s1qy0c.js → speckit-checkoff-wfsbvt1b.js} +5 -5
- package/dist/cli/{workspace-snapshot-xwhhznt3.js → workspace-snapshot-553c6cd3.js} +3 -3
- package/dist/cli/{worktree-collision-ownership-19n9em8b.js → worktree-collision-ownership-4znww3nt.js} +11 -11
- package/dist/cli/{worktree-isolation-hkd7xtdp.js → worktree-isolation-w4k8q59z.js} +38 -38
- package/dist/commands/registry.d.ts +1 -1
- package/dist/gate-evidence.d.ts +51 -0
- package/dist/index.js +34 -34
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/manifest.d.ts +1 -0
- package/dist/tools/record-branch-freshness.d.ts +6 -0
- package/dist/tools/record-trace-validation.d.ts +11 -0
- package/dist/tools/recover-stage-a-task.d.ts +5 -0
- package/dist/tools/tool-metadata.d.ts +4 -0
- package/dist/workflow/settlement-recovery.d.ts +52 -0
- package/dist/workflow/stage-a-repair.d.ts +13 -8
- package/dist/workflow/task-recovery-status.d.ts +11 -2
- package/package.json +1 -1
- package/dist/cli/curator-llm-factory-782jtn0a.js +0 -75
- package/dist/cli/guardrail-explain-xgahzbts.js +0 -76
|
@@ -10,20 +10,85 @@ to_shell_path() {
|
|
|
10
10
|
printf '%s\n' "$1"
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
to_native_path() {
|
|
14
|
+
case "$(uname -s 2>/dev/null || true)" in
|
|
15
|
+
MINGW* | MSYS* | CYGWIN*)
|
|
16
|
+
command -v cygpath >/dev/null 2>&1 || return 1
|
|
17
|
+
cygpath -w "$1"
|
|
18
|
+
;;
|
|
19
|
+
*) printf '%s\n' "$1" ;;
|
|
20
|
+
esac
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
root="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "repro-check: not inside a git work tree" >&2; exit 2; }
|
|
14
24
|
root="$(to_shell_path "$root")"
|
|
15
25
|
root_real="$(cd "$root" && pwd -P)"
|
|
16
26
|
script_dir="$(cd "$(dirname "$0")" && pwd -P)"
|
|
27
|
+
trace_root_real=""
|
|
17
28
|
|
|
18
|
-
usage() { echo "usage: repro-check.sh {run|checkpoint|verify-checkpoint} --slug <slug> ..." >&2; exit 2; }
|
|
29
|
+
usage() { echo "usage: repro-check.sh {run|checkpoint|verify-checkpoint|verify-semantics|anchor|verify-anchor} --slug <slug> ..." >&2; exit 2; }
|
|
19
30
|
valid_slug() { case "$1" in ''|*[!a-z0-9-]*) return 1;; *) return 0;; esac; }
|
|
20
|
-
valid_id() {
|
|
31
|
+
valid_id() {
|
|
32
|
+
local suffix
|
|
33
|
+
case "$1" in
|
|
34
|
+
C[0-9]*) suffix="${1#C}"; case "$suffix" in ''|*[!0-9]*) return 1;; esac; return 0;;
|
|
35
|
+
*) return 1;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
21
38
|
has_bad_field() { case "$1" in *$'\t'*|*$'\n'*|*$'\r'*) return 0;; *) return 1;; esac; }
|
|
39
|
+
# Unlike a shell option, a regex beginning with `--` is valid input. Keep it
|
|
40
|
+
# as data for grep by using `--` at the option/operand boundary. Reject control
|
|
41
|
+
# bytes before echoing the regex in the result artifact.
|
|
42
|
+
has_bad_control() {
|
|
43
|
+
# Shell variables can carry newlines, but grep treats them as record
|
|
44
|
+
# separators; reject those explicitly before scanning the remaining bytes.
|
|
45
|
+
case "$1" in *$'\t'*|*$'\n'*|*$'\r'*) return 0;; esac
|
|
46
|
+
local matches
|
|
47
|
+
# grep -c drains stdin. A terminal grep -q can make printf receive SIGPIPE
|
|
48
|
+
# under inherited pipefail, turning a real control byte into a false miss.
|
|
49
|
+
matches="$(LC_ALL=C printf '%s' "$1" | LC_ALL=C grep -c '[[:cntrl:]]' || true)"
|
|
50
|
+
[ "${matches:-0}" -gt 0 ]
|
|
51
|
+
}
|
|
52
|
+
# POSIX pathnames may contain tabs/newlines, but trace paths are also rendered
|
|
53
|
+
# in diagnostics. Reject every C0/DEL byte at the checkpoint boundary so an
|
|
54
|
+
# ANSI escape or other control byte cannot become terminal-visible evidence.
|
|
55
|
+
has_bad_path() {
|
|
56
|
+
has_bad_control "$1"
|
|
57
|
+
}
|
|
58
|
+
is_sha1() { printf '%s' "$1" | grep -Eq '^[0-9a-f]{40}$'; }
|
|
22
59
|
is_inside_root() {
|
|
23
60
|
case "$1" in /*|[A-Za-z]:*|*\\*) return 1;; esac
|
|
24
61
|
case "/$1/" in */../*|*/./*) return 1;; esac
|
|
25
62
|
return 0
|
|
26
63
|
}
|
|
64
|
+
|
|
65
|
+
# Resolve a repository-relative path without following a symlink or junction in
|
|
66
|
+
# any component. `-f`, `cp`, and `git hash-object` all follow links, so checking
|
|
67
|
+
# only the parent directory is insufficient: a link leaf could copy or freeze
|
|
68
|
+
# bytes from outside the repository and later make verification report a false
|
|
69
|
+
# match. Use the canonical repository root as the walk anchor so a symlinked
|
|
70
|
+
# checkout path does not weaken the component checks.
|
|
71
|
+
repo_path_safe() {
|
|
72
|
+
local rel="$1" current part
|
|
73
|
+
is_inside_root "$rel" || return 1
|
|
74
|
+
current="$root_real"
|
|
75
|
+
while IFS= read -r part; do
|
|
76
|
+
[ -n "$part" ] || continue
|
|
77
|
+
current="$current/$part"
|
|
78
|
+
[ -L "$current" ] && return 1
|
|
79
|
+
done <<EOF
|
|
80
|
+
$(printf '%s' "$rel" | tr '/' '\n')
|
|
81
|
+
EOF
|
|
82
|
+
[ -e "$current" ] || return 1
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
repo_file_safe() {
|
|
87
|
+
local rel="$1"
|
|
88
|
+
repo_path_safe "$rel" || return 1
|
|
89
|
+
[ -f "$root_real/$rel" ] || return 1
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
27
92
|
issue_traces_base="$root_real/.agents/issue-traces"
|
|
28
93
|
|
|
29
94
|
# Path-only preflight: trace_dir (default or --trace-dir override) must be
|
|
@@ -31,6 +96,10 @@ issue_traces_base="$root_real/.agents/issue-traces"
|
|
|
31
96
|
# filesystem. Runs before any mkdir/write, so a caller cannot point the
|
|
32
97
|
# script at an arbitrary path via that flag.
|
|
33
98
|
validate_trace_dir_prefix() {
|
|
99
|
+
has_bad_path "$trace_dir" && {
|
|
100
|
+
echo "repro-check: --trace-dir cannot contain control bytes" >&2
|
|
101
|
+
exit 2
|
|
102
|
+
}
|
|
34
103
|
case "$trace_dir/" in
|
|
35
104
|
"$root"/.agents/issue-traces/*) ;;
|
|
36
105
|
*)
|
|
@@ -45,12 +114,19 @@ validate_trace_dir_prefix() {
|
|
|
45
114
|
# run before any mkdir -p, since mkdir -p happily follows an existing
|
|
46
115
|
# symlinked ancestor before any later check on the final path can catch it.
|
|
47
116
|
refuse_ancestor_symlink_escape() {
|
|
48
|
-
local check="$trace_dir" resolved
|
|
49
|
-
while
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
117
|
+
local check="${1:-$trace_dir}" resolved parent
|
|
118
|
+
while :; do
|
|
119
|
+
# `-e` is false for a dangling link. Test `-L` before deciding whether to
|
|
120
|
+
# ascend so a broken link cannot be skipped and later followed by mkdir -p.
|
|
121
|
+
if [ -L "$check" ]; then
|
|
122
|
+
echo "repro-check: --trace-dir must be inside .agents/issue-traces" >&2
|
|
123
|
+
exit 2
|
|
124
|
+
fi
|
|
125
|
+
[ -e "$check" ] && break
|
|
126
|
+
parent="$(dirname "$check")"
|
|
127
|
+
[ "$parent" != "$check" ] || break
|
|
128
|
+
check="$parent"
|
|
129
|
+
done
|
|
54
130
|
resolved="$(cd "$check" && pwd -P)"
|
|
55
131
|
case "$resolved/" in
|
|
56
132
|
"$root_real"/*) ;;
|
|
@@ -82,6 +158,52 @@ require_contained() {
|
|
|
82
158
|
esac
|
|
83
159
|
}
|
|
84
160
|
|
|
161
|
+
# Validate a trace artifact immediately before reading or hashing it. Shell
|
|
162
|
+
# tests such as `[ -f ]` and `git hash-object` follow symlinks, so walk every
|
|
163
|
+
# component first and require the canonical parent to remain beneath the
|
|
164
|
+
# canonical trace root. This rejects both POSIX symlinks and Windows
|
|
165
|
+
# junctions surfaced by MSYS as `test -L`, including dangling leaf links.
|
|
166
|
+
trace_path_safe() {
|
|
167
|
+
local path="$1" kind="${2:-file}" ancestor parent resolved
|
|
168
|
+
[ -n "$trace_root_real" ] || { echo "repro-check: trace root is not initialized" >&2; exit 2; }
|
|
169
|
+
case "$path/" in
|
|
170
|
+
"$trace_root_real/"*) ;;
|
|
171
|
+
*) echo "repro-check: refusing path outside canonical trace root: $path" >&2; exit 2;;
|
|
172
|
+
esac
|
|
173
|
+
case "$path/" in
|
|
174
|
+
*/../*|*/./*) echo "repro-check: refusing ambiguous trace path: $path" >&2; exit 2;;
|
|
175
|
+
esac
|
|
176
|
+
ancestor="$path"
|
|
177
|
+
while [ "$ancestor" != "$trace_root_real" ] && [ "$ancestor" != "/" ]; do
|
|
178
|
+
if [ -L "$ancestor" ]; then
|
|
179
|
+
echo "repro-check: refusing symlinked trace component: $ancestor" >&2
|
|
180
|
+
exit 2
|
|
181
|
+
fi
|
|
182
|
+
ancestor="$(dirname "$ancestor")"
|
|
183
|
+
done
|
|
184
|
+
[ "$ancestor" = "$trace_root_real" ] || { echo "repro-check: trace path is not rooted at canonical trace directory: $path" >&2; exit 2; }
|
|
185
|
+
[ "$path" = "$trace_root_real" ] && { [ "$kind" = dir ] && [ -d "$path" ]; return $?; }
|
|
186
|
+
parent="$(dirname "$path")"
|
|
187
|
+
[ -d "$parent" ] || return 1
|
|
188
|
+
resolved="$(cd "$parent" 2>/dev/null && pwd -P)" || { echo "repro-check: could not resolve trace artifact parent: $path" >&2; exit 2; }
|
|
189
|
+
case "$resolved/" in
|
|
190
|
+
"$trace_root_real/"*) ;;
|
|
191
|
+
*) echo "repro-check: trace artifact parent resolves outside canonical trace root: $path" >&2; exit 2;;
|
|
192
|
+
esac
|
|
193
|
+
case "$kind" in
|
|
194
|
+
file) [ -f "$path" ] || return 1;;
|
|
195
|
+
dir) [ -d "$path" ] || return 1;;
|
|
196
|
+
*) echo "repro-check: internal invalid trace path kind: $kind" >&2; exit 2;;
|
|
197
|
+
esac
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
set_trace_root() {
|
|
201
|
+
require_contained "$trace_dir"
|
|
202
|
+
[ -d "$trace_dir" ] || { echo "repro-check: trace directory is missing: $trace_dir" >&2; exit 2; }
|
|
203
|
+
trace_root_real="$(cd "$trace_dir" 2>/dev/null && pwd -P)" || { echo "repro-check: could not resolve trace directory: $trace_dir" >&2; exit 2; }
|
|
204
|
+
trace_path_safe "$trace_root_real" dir >/dev/null
|
|
205
|
+
}
|
|
206
|
+
|
|
85
207
|
trace_for() {
|
|
86
208
|
[ -n "$trace_dir" ] || trace_dir="$root/.agents/issue-traces/$slug"
|
|
87
209
|
validate_trace_dir_prefix
|
|
@@ -106,9 +228,11 @@ manifest_for() { trace_for; printf '%s\n' "$trace_dir/repro/checkpoint.manifest"
|
|
|
106
228
|
#
|
|
107
229
|
# The legacy header with no `rows=` count (`... v1`) is REJECTED rather than
|
|
108
230
|
# accepted for compatibility: accepting it would itself be a one-line bypass
|
|
109
|
-
# (write the old header, then truncate freely).
|
|
110
|
-
#
|
|
111
|
-
#
|
|
231
|
+
# (write the old header, then truncate freely). Existing v3 manifests can,
|
|
232
|
+
# however, contain an AMEND row with the historical FORMAT_ONLY reason. That
|
|
233
|
+
# reason is accepted only by verification (and by a later, newly reasoned
|
|
234
|
+
# amendment while recovering such a trace); do_checkpoint never accepts it as
|
|
235
|
+
# a new reason, so FORMAT_ONLY cannot be introduced through this script.
|
|
112
236
|
#
|
|
113
237
|
# This is tamper-EVIDENCE, not tamper-proofing: the trace directory is the
|
|
114
238
|
# agent's own write surface, so a rewriter that renumbers every row AND
|
|
@@ -117,8 +241,17 @@ manifest_for() { trace_for; printf '%s\n' "$trace_dir/repro/checkpoint.manifest"
|
|
|
117
241
|
# existence or completeness to anything outside it). What it stops is a
|
|
118
242
|
# partial write or a hand edit that leaves the file internally inconsistent.
|
|
119
243
|
validate_manifest() {
|
|
120
|
-
local file="$1" problem counts
|
|
121
|
-
|
|
244
|
+
local file="$1" allow_conflicts="${2:-}" allow_legacy_format_only="${3:-}" problem counts header
|
|
245
|
+
trace_path_safe "$file" file || { echo "repro-check: checkpoint manifest missing or unsafe" >&2; exit 2; }
|
|
246
|
+
# `awk` on some supported shells normalizes CRLF records, so inspect the
|
|
247
|
+
# raw header first. A CRLF manifest is not byte-valid for the checkpoint
|
|
248
|
+
# format and must fail as a malformed header rather than reaching replay and
|
|
249
|
+
# reporting a stale digest.
|
|
250
|
+
header=""
|
|
251
|
+
IFS= read -r header < "$file" || true
|
|
252
|
+
case "$header" in
|
|
253
|
+
*$'\r'*) problem="header" ;;
|
|
254
|
+
*) problem="$(awk -F '\t' -v allow_conflicts="$allow_conflicts" -v allow_legacy_format_only="$allow_legacy_format_only" '
|
|
122
255
|
NR == 1 {
|
|
123
256
|
if ($0 !~ /^# issue-tracer checkpoint manifest v1 rows=[0-9]+$/) { bad = "header"; exit }
|
|
124
257
|
declared = $0
|
|
@@ -130,26 +263,51 @@ validate_manifest() {
|
|
|
130
263
|
rows += 1
|
|
131
264
|
if (NF != 10) { bad = "fields " NR; exit }
|
|
132
265
|
if ($1 "" != rows "") { bad = "seq " NR; exit }
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
266
|
+
if ($3 ~ /[[:cntrl:]]/) { bad = "unsafe-path"; exit }
|
|
267
|
+
if ($2 != "CHECKPOINT" && $2 != "AMEND") { bad = "kind " NR; exit }
|
|
268
|
+
if (($2 == "CHECKPOINT" && $10 != "-") || ($2 == "AMEND" && $10 != "CHECK_WRONG" && $10 != "AC_CHANGED_BY_USER" && !(allow_legacy_format_only == "allow-legacy-format-only" && $10 == "FORMAT_ONLY"))) { bad = "reason " NR; exit }
|
|
269
|
+
# A pair is frozen by its first row; every later row for that exact
|
|
270
|
+
# (path, check-id) pair must be a reasoned AMEND. A path may legitimately
|
|
271
|
+
# carry multiple checks, so path alone is not an identity key here.
|
|
272
|
+
# Length-prefix the path so the composite key remains injective even if
|
|
273
|
+
# a legal path contains the awk SUBSEP byte.
|
|
274
|
+
pair = length($3) ":" $3 ":" $6
|
|
275
|
+
if ($2 == "CHECKPOINT" && seen_pair[pair]) { bad = "duplicate " NR; exit }
|
|
276
|
+
if ($2 == "AMEND" && !seen_pair[pair]) { bad = "orphan " NR; exit }
|
|
277
|
+
seen_pair[pair] = 1
|
|
278
|
+
latest_path[pair] = $3
|
|
279
|
+
latest_blob[pair] = $4
|
|
141
280
|
}
|
|
142
281
|
END {
|
|
143
282
|
if (bad != "") { print bad; exit }
|
|
144
283
|
if (NR == 0) { print "header"; exit }
|
|
284
|
+
# The effective manifest is one latest row per pair. A checkpoint tree
|
|
285
|
+
# has one blob per path, so identical blobs for multiple checks are
|
|
286
|
+
# safely deduplicable but divergent effective blobs are invalid rather
|
|
287
|
+
# than silently becoming last-writer-wins by path.
|
|
288
|
+
for (pair in latest_path) {
|
|
289
|
+
path = latest_path[pair]
|
|
290
|
+
if (effective_seen[path] && effective_blob[path] != latest_blob[pair] && allow_conflicts != "allow-conflicts") {
|
|
291
|
+
print "conflict " path
|
|
292
|
+
exit
|
|
293
|
+
}
|
|
294
|
+
effective_seen[path] = 1
|
|
295
|
+
effective_blob[path] = latest_blob[pair]
|
|
296
|
+
}
|
|
145
297
|
if (declared != rows + 0) { print "count " declared " " rows + 0 }
|
|
146
298
|
}
|
|
147
|
-
' "$file")"
|
|
299
|
+
' "$file")" ;;
|
|
300
|
+
esac
|
|
148
301
|
case "$problem" in
|
|
149
302
|
'') return 0 ;;
|
|
150
303
|
'fields '*) echo "repro-check: checkpoint manifest line ${problem#fields } does not have 10 tab-separated fields" >&2 ;;
|
|
151
304
|
'seq '*) echo "repro-check: checkpoint manifest seq is not contiguous (row deleted or reordered) at line ${problem#seq }" >&2 ;;
|
|
152
|
-
'
|
|
305
|
+
'duplicate '*) echo "repro-check: checkpoint manifest line ${problem#duplicate } duplicates an existing CHECKPOINT pair (re-freezes without an AMEND reason)" >&2 ;;
|
|
306
|
+
'orphan '*) echo "repro-check: checkpoint manifest line ${problem#orphan } AMENDs an unknown path/check-id pair" >&2 ;;
|
|
307
|
+
'unsafe-path') echo "repro-check: checkpoint manifest contains a path with control bytes" >&2 ;;
|
|
308
|
+
'conflict '*) echo "repro-check: checkpoint manifest has conflicting effective blobs for path ${problem#conflict }" >&2 ;;
|
|
309
|
+
'kind '*) echo "repro-check: checkpoint manifest line ${problem#kind } has an invalid kind (use CHECKPOINT or AMEND)" >&2 ;;
|
|
310
|
+
'reason '*) echo "repro-check: checkpoint manifest line ${problem#reason } has an invalid reason for its kind" >&2 ;;
|
|
153
311
|
'count '*)
|
|
154
312
|
counts="${problem#count }"
|
|
155
313
|
echo "repro-check: checkpoint manifest header records ${counts%% *} rows, found ${counts#* } (rows deleted or truncated)" >&2
|
|
@@ -159,10 +317,13 @@ validate_manifest() {
|
|
|
159
317
|
exit 2
|
|
160
318
|
}
|
|
161
319
|
|
|
162
|
-
# True when
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
320
|
+
# True when an exact (path, check-id) pair already appears in the manifest.
|
|
321
|
+
# Multiple checks may share a path, but a pair can only be introduced once and
|
|
322
|
+
# then superseded by an AMEND row.
|
|
323
|
+
manifest_has_pair() {
|
|
324
|
+
trace_path_safe "$1" file || return 1
|
|
325
|
+
awk -F '\t' -v want_path="$2" -v want_check="$3" \
|
|
326
|
+
'NR > 1 && $3 == want_path && $6 == want_check { found = 1; exit } END { exit !found }' "$1"
|
|
166
327
|
}
|
|
167
328
|
|
|
168
329
|
# Append one data row and restamp the header's recorded count in a single
|
|
@@ -175,13 +336,16 @@ manifest_has_path() {
|
|
|
175
336
|
append_manifest_row() {
|
|
176
337
|
local file="$1" count="$2" row="$3" temp
|
|
177
338
|
temp="$file.append.tmp"
|
|
339
|
+
trace_path_safe "$(dirname "$file")" dir || { echo "repro-check: manifest directory is missing or unsafe" >&2; exit 2; }
|
|
340
|
+
trace_path_safe "$file" file || true
|
|
178
341
|
refuse_nonregular_target "$file"
|
|
179
|
-
|
|
342
|
+
open_exclusive_target "$temp" || { echo "repro-check: could not create manifest temp file safely" >&2; exit 2; }
|
|
180
343
|
{
|
|
181
344
|
printf '# issue-tracer checkpoint manifest v1 rows=%s\n' "$count"
|
|
182
345
|
awk 'NR > 1' "$file"
|
|
183
346
|
printf '%s\n' "$row"
|
|
184
|
-
}
|
|
347
|
+
} >&"$exclusive_fd"
|
|
348
|
+
close_exclusive_target
|
|
185
349
|
mv "$temp" "$file"
|
|
186
350
|
}
|
|
187
351
|
|
|
@@ -199,31 +363,88 @@ refuse_nonregular_target() {
|
|
|
199
363
|
fi
|
|
200
364
|
}
|
|
201
365
|
|
|
366
|
+
# Open a new regular file and retain a descriptor. `exec {var}>...` would be
|
|
367
|
+
# convenient, but automatic brace descriptor allocation is Bash 4+ syntax and
|
|
368
|
+
# breaks the system Bash 3.2 shipped with macOS. Probe a small reserved range
|
|
369
|
+
# of descriptors instead; callers write through $exclusive_fd and close it
|
|
370
|
+
# before publishing the path with rename/mv. The parent directory is checked
|
|
371
|
+
# by the caller; portable shell code cannot hold an open directory descriptor
|
|
372
|
+
# across all supported Bash/MSYS environments, so the final rename remains
|
|
373
|
+
# the containment boundary.
|
|
374
|
+
open_exclusive_target() {
|
|
375
|
+
local path="$1" fd
|
|
376
|
+
refuse_nonregular_target "$path"
|
|
377
|
+
set -C
|
|
378
|
+
for fd in 9 10 11 12 13 14 15 16 17 18 19; do
|
|
379
|
+
# A successful duplication means the descriptor is already in use; leave
|
|
380
|
+
# it untouched and try the next reserved descriptor.
|
|
381
|
+
if true >&"$fd" 2>/dev/null; then
|
|
382
|
+
continue
|
|
383
|
+
fi
|
|
384
|
+
case "$fd" in
|
|
385
|
+
9) exec 9>"$path" && exclusive_fd=9 && set +C && return 0 ;;
|
|
386
|
+
10) exec 10>"$path" && exclusive_fd=10 && set +C && return 0 ;;
|
|
387
|
+
11) exec 11>"$path" && exclusive_fd=11 && set +C && return 0 ;;
|
|
388
|
+
12) exec 12>"$path" && exclusive_fd=12 && set +C && return 0 ;;
|
|
389
|
+
13) exec 13>"$path" && exclusive_fd=13 && set +C && return 0 ;;
|
|
390
|
+
14) exec 14>"$path" && exclusive_fd=14 && set +C && return 0 ;;
|
|
391
|
+
15) exec 15>"$path" && exclusive_fd=15 && set +C && return 0 ;;
|
|
392
|
+
16) exec 16>"$path" && exclusive_fd=16 && set +C && return 0 ;;
|
|
393
|
+
17) exec 17>"$path" && exclusive_fd=17 && set +C && return 0 ;;
|
|
394
|
+
18) exec 18>"$path" && exclusive_fd=18 && set +C && return 0 ;;
|
|
395
|
+
19) exec 19>"$path" && exclusive_fd=19 && set +C && return 0 ;;
|
|
396
|
+
esac
|
|
397
|
+
done
|
|
398
|
+
set +C
|
|
399
|
+
return 1
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
close_exclusive_target() {
|
|
403
|
+
case "${exclusive_fd:-}" in
|
|
404
|
+
9) exec 9>&- ;;
|
|
405
|
+
10) exec 10>&- ;;
|
|
406
|
+
11) exec 11>&- ;;
|
|
407
|
+
12) exec 12>&- ;;
|
|
408
|
+
13) exec 13>&- ;;
|
|
409
|
+
14) exec 14>&- ;;
|
|
410
|
+
15) exec 15>&- ;;
|
|
411
|
+
16) exec 16>&- ;;
|
|
412
|
+
17) exec 17>&- ;;
|
|
413
|
+
18) exec 18>&- ;;
|
|
414
|
+
19) exec 19>&- ;;
|
|
415
|
+
esac
|
|
416
|
+
exclusive_fd=""
|
|
417
|
+
}
|
|
418
|
+
|
|
202
419
|
bound_log() {
|
|
203
420
|
local log="$1" total kept=1048576 omitted temp
|
|
204
421
|
total="$(wc -c < "$log" | tr -d ' ')"
|
|
205
422
|
[ "$total" -le 2097152 ] && return
|
|
206
423
|
temp="$log.truncate.tmp"
|
|
207
|
-
|
|
208
|
-
{ head -c "$kept" "$log"; printf '\n[... truncated %s bytes ...]\n' "$((total - (kept * 2)))"; tail -c "$kept" "$log"; }
|
|
424
|
+
open_exclusive_target "$temp" || { echo "repro-check: could not create log temp file safely" >&2; exit 2; }
|
|
425
|
+
{ head -c "$kept" "$log"; printf '\n[... truncated %s bytes ...]\n' "$((total - (kept * 2)))"; tail -c "$kept" "$log"; } >&"$exclusive_fd"
|
|
426
|
+
close_exclusive_target
|
|
209
427
|
mv "$temp" "$log"
|
|
210
428
|
}
|
|
211
429
|
|
|
212
430
|
run_one() {
|
|
213
431
|
local cwd="$1" log="$2" seconds="$3"; shift 3
|
|
214
|
-
local status=0 pid started timed=0 waited=0
|
|
432
|
+
local status=0 pid started timed=0 waited=0 temp parent
|
|
433
|
+
parent="$(dirname "$log")"
|
|
434
|
+
trace_path_safe "$parent" dir || { echo "repro-check: log parent is missing or unsafe" >&2; exit 2; }
|
|
215
435
|
refuse_nonregular_target "$log"
|
|
216
|
-
|
|
436
|
+
temp="$log.run.tmp.$$.$RANDOM"
|
|
437
|
+
open_exclusive_target "$temp" || { echo "repro-check: could not create log temp file safely" >&2; exit 2; }
|
|
217
438
|
if [ "${REPRO_CHECK_FORCE_FALLBACK:-0}" != "1" ] && command -v timeout >/dev/null 2>&1; then
|
|
218
|
-
( cd "$cwd" && timeout --foreground -k 5 "${seconds}s" "$@" )
|
|
439
|
+
( cd "$cwd" && timeout --foreground -k 5 "${seconds}s" "$@" ) >&"$exclusive_fd" 2>&1 || status=$?
|
|
219
440
|
else
|
|
220
441
|
# POSIX watchdog fallback: run the child in its own process group so the
|
|
221
442
|
# whole group (not just the immediate child) can be killed on timeout.
|
|
222
443
|
if command -v setsid >/dev/null 2>&1; then
|
|
223
|
-
( cd "$cwd" && exec setsid "$@" )
|
|
444
|
+
( cd "$cwd" && exec setsid "$@" ) >&"$exclusive_fd" 2>&1 &
|
|
224
445
|
else
|
|
225
446
|
set -m
|
|
226
|
-
( cd "$cwd" && "$@" )
|
|
447
|
+
( cd "$cwd" && "$@" ) >&"$exclusive_fd" 2>&1 &
|
|
227
448
|
set +m
|
|
228
449
|
fi
|
|
229
450
|
pid=$!
|
|
@@ -242,31 +463,98 @@ run_one() {
|
|
|
242
463
|
wait "$pid" 2>/dev/null || status=$?
|
|
243
464
|
[ "$timed" -eq 0 ] || status=124
|
|
244
465
|
fi
|
|
466
|
+
close_exclusive_target
|
|
467
|
+
trace_path_safe "$parent" dir || { echo "repro-check: log parent became unsafe" >&2; exit 2; }
|
|
468
|
+
refuse_nonregular_target "$log"
|
|
469
|
+
mv "$temp" "$log"
|
|
245
470
|
bound_log "$log"
|
|
246
471
|
printf '%s\n' "$status"
|
|
247
472
|
}
|
|
248
473
|
|
|
474
|
+
worktree_path_safe() {
|
|
475
|
+
local rel="$1" current part resolved target
|
|
476
|
+
is_inside_root "$rel" || return 1
|
|
477
|
+
[ -n "${worktree_real:-}" ] || return 1
|
|
478
|
+
current="$worktree_real"
|
|
479
|
+
while IFS= read -r part; do
|
|
480
|
+
[ -n "$part" ] || continue
|
|
481
|
+
current="$current/$part"
|
|
482
|
+
[ -L "$current" ] && return 1
|
|
483
|
+
if [ -e "$current" ]; then
|
|
484
|
+
if [ -d "$current" ]; then
|
|
485
|
+
resolved="$(cd "$current" 2>/dev/null && pwd -P)" || return 1
|
|
486
|
+
case "$resolved/" in
|
|
487
|
+
"$worktree_real/"*) ;;
|
|
488
|
+
*) return 1 ;;
|
|
489
|
+
esac
|
|
490
|
+
elif [ "$current" != "$worktree_real/$rel" ] || [ ! -f "$current" ]; then
|
|
491
|
+
return 1
|
|
492
|
+
fi
|
|
493
|
+
fi
|
|
494
|
+
done <<EOF
|
|
495
|
+
$(printf '%s' "$rel" | tr '/' '\n')
|
|
496
|
+
EOF
|
|
497
|
+
target="$worktree_real/$rel"
|
|
498
|
+
[ ! -L "$target" ] || return 1
|
|
499
|
+
return 0
|
|
500
|
+
}
|
|
501
|
+
|
|
249
502
|
copy_path() {
|
|
250
|
-
local rel="$1" source target source_dir
|
|
503
|
+
local rel="$1" source target source_dir target_parent
|
|
251
504
|
is_inside_root "$rel" || { echo "repro-check: --copy path must be repo-relative without ..: $rel" >&2; exit 2; }
|
|
252
|
-
|
|
253
|
-
|
|
505
|
+
repo_path_safe "$rel" || { echo "repro-check: --copy path must remain inside repo without symlink components: $rel" >&2; exit 2; }
|
|
506
|
+
source="$root_real/$rel"
|
|
254
507
|
source_dir="$(cd "$(dirname "$source")" && pwd -P)"
|
|
255
508
|
case "$source_dir" in "$root_real"|"$root_real"/*) ;; *) echo "repro-check: --copy path resolves outside repo: $rel" >&2; exit 2;; esac
|
|
256
|
-
target="$
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
509
|
+
target="$worktree_real/$rel"
|
|
510
|
+
target_parent="$(dirname "$target")"
|
|
511
|
+
worktree_path_safe "$rel" || { echo "repro-check: --copy target must remain inside disposable worktree: $rel" >&2; exit 2; }
|
|
512
|
+
mkdir -p -- "$target_parent"
|
|
513
|
+
worktree_path_safe "$rel" || { echo "repro-check: --copy target became unsafe while preparing destination: $rel" >&2; exit 2; }
|
|
514
|
+
rm -rf -- "$target"
|
|
515
|
+
worktree_path_safe "$rel" || { echo "repro-check: --copy target became unsafe before copy: $rel" >&2; exit 2; }
|
|
516
|
+
cp -R -- "$source" "$target"
|
|
517
|
+
worktree_path_safe "$rel" || { echo "repro-check: --copy target became unsafe after copy: $rel" >&2; exit 2; }
|
|
260
518
|
}
|
|
261
519
|
|
|
262
520
|
link_deps() {
|
|
263
521
|
[ "$deps" = link ] || return 0
|
|
264
522
|
[ -e "$root/node_modules" ] || return 0
|
|
265
|
-
|
|
523
|
+
local source_real linked_real
|
|
524
|
+
source_real="$(cd "$root/node_modules" 2>/dev/null && pwd -P)" || { echo "repro-check: dependency source cannot be resolved" >&2; return 1; }
|
|
525
|
+
# An existing directory, junction, or symlink is not silently accepted: it
|
|
526
|
+
# must resolve to the exact dependency source this run would have linked.
|
|
527
|
+
# This also rejects dangling links, which otherwise look absent to `-e`.
|
|
528
|
+
if [ -e "$worktree/node_modules" ] || [ -L "$worktree/node_modules" ]; then
|
|
529
|
+
linked_real="$(cd "$worktree/node_modules" 2>/dev/null && pwd -P)" || {
|
|
530
|
+
echo "repro-check: existing dependency target cannot be resolved" >&2
|
|
531
|
+
return 1
|
|
532
|
+
}
|
|
533
|
+
if [ "$source_real" = "$linked_real" ]; then
|
|
534
|
+
return 0
|
|
535
|
+
fi
|
|
536
|
+
echo "repro-check: existing dependency target resolves to '$linked_real', expected '$source_real'" >&2
|
|
537
|
+
return 1
|
|
538
|
+
fi
|
|
266
539
|
case "$(uname -s 2>/dev/null || true)" in
|
|
267
|
-
MINGW* | MSYS* | CYGWIN*)
|
|
268
|
-
|
|
540
|
+
MINGW* | MSYS* | CYGWIN*)
|
|
541
|
+
local destination source
|
|
542
|
+
destination="$(to_native_path "$worktree/node_modules")" || { echo "repro-check: cygpath is required to create a Windows dependency junction" >&2; return 1; }
|
|
543
|
+
source="$(to_native_path "$root/node_modules")" || { echo "repro-check: cygpath is required to create a Windows dependency junction" >&2; return 1; }
|
|
544
|
+
if ! command -v cmd >/dev/null 2>&1 || ! cmd //c mklink //J "$destination" "$source" >/dev/null 2>&1; then
|
|
545
|
+
echo "repro-check: could not create dependency junction: $destination -> $source" >&2
|
|
546
|
+
return 1
|
|
547
|
+
fi
|
|
548
|
+
;;
|
|
549
|
+
*)
|
|
550
|
+
ln -s "$root/node_modules" "$worktree/node_modules" || { echo "repro-check: could not link dependency directory" >&2; return 1; }
|
|
551
|
+
;;
|
|
269
552
|
esac
|
|
553
|
+
linked_real="$(cd "$worktree/node_modules" 2>/dev/null && pwd -P)" || { echo "repro-check: dependency link cannot be resolved" >&2; return 1; }
|
|
554
|
+
if [ "$source_real" != "$linked_real" ]; then
|
|
555
|
+
echo "repro-check: dependency link resolved to '$linked_real', expected '$source_real'" >&2
|
|
556
|
+
return 1
|
|
557
|
+
fi
|
|
270
558
|
}
|
|
271
559
|
|
|
272
560
|
quoted_argv() { local arg; for arg in "$@"; do printf '%q ' "$arg"; done; }
|
|
@@ -298,25 +586,31 @@ do_run() {
|
|
|
298
586
|
case "$deps" in link|none) ;; *) echo "repro-check: --deps must be link or none" >&2; exit 2;; esac
|
|
299
587
|
case "$timeout_seconds" in ''|*[!0-9]*|0) echo "repro-check: --timeout must be a positive integer" >&2; exit 2;; esac
|
|
300
588
|
case "$class" in DISCRIMINATING|NEW-SURFACE) [ -n "$expect" ] || { echo "repro-check: --expect is required for $class" >&2; exit 2; };; esac
|
|
301
|
-
|
|
589
|
+
has_bad_control "$expect" && { echo "repro-check: --expect cannot contain control bytes" >&2; exit 2; }
|
|
302
590
|
trace_for
|
|
303
591
|
require_contained "$trace_dir"
|
|
304
592
|
require_contained "$trace_dir/repro"
|
|
593
|
+
refuse_ancestor_symlink_escape "$trace_dir/repro"
|
|
305
594
|
mkdir -p "$trace_dir/repro"
|
|
306
595
|
require_contained "$trace_dir/repro"
|
|
596
|
+
set_trace_root
|
|
597
|
+
trace_path_safe "$trace_dir/repro" dir || { echo "repro-check: repro directory is missing or unsafe" >&2; exit 2; }
|
|
307
598
|
worktree="$(mktemp -d "${TMPDIR:-/tmp}/issue-tracer-repro.XXXXXX")"
|
|
308
599
|
cleanup() { git worktree remove --force "$worktree" >/dev/null 2>&1 || true; rm -rf "$worktree"; }
|
|
309
600
|
trap cleanup EXIT HUP INT TERM
|
|
310
601
|
git worktree add --detach "$worktree" "$base" >/dev/null 2>&1 || { echo "repro-check: could not create disposable worktree" >&2; exit 2; }
|
|
602
|
+
worktree_real="$(cd "$worktree" 2>/dev/null && pwd -P)" || { echo "repro-check: disposable worktree cannot be resolved" >&2; exit 2; }
|
|
311
603
|
for arg in ${copies[@]+"${copies[@]}"}; do copy_path "$arg"; done
|
|
312
604
|
link_deps
|
|
313
605
|
base_status="$(run_one "$worktree" "$trace_dir/repro/$check_id.base.log" "$timeout_seconds" "$@")"
|
|
606
|
+
trace_path_safe "$trace_dir/repro/$check_id.base.log" file || { echo "repro-check: base log was not created safely" >&2; exit 2; }
|
|
314
607
|
head_status="$(run_one "$root" "$trace_dir/repro/$check_id.head.log" "$timeout_seconds" "$@")"
|
|
608
|
+
trace_path_safe "$trace_dir/repro/$check_id.head.log" file || { echo "repro-check: head log was not created safely" >&2; exit 2; }
|
|
315
609
|
if [ "$base_status" -eq 124 ] || [ "$head_status" -eq 124 ]; then
|
|
316
610
|
base_result="TIMEOUT"; head_result="TIMEOUT"; verdict="FAIL"; exit_code=6
|
|
317
611
|
elif [ "$class" = DISCRIMINATING ]; then
|
|
318
612
|
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
|
|
613
|
+
elif trace_path_safe "$trace_dir/repro/$check_id.base.log" file && grep -Eq -- "$expect" "$trace_dir/repro/$check_id.base.log"; then
|
|
320
614
|
base_result="RED"
|
|
321
615
|
if [ "$head_status" -eq 0 ]; then head_result="GREEN"; verdict="PASS"; else head_result="FAIL"; verdict="FAIL"; exit_code=5; fi
|
|
322
616
|
else base_result="ERROR"; verdict="ERROR"; exit_code=3; fi
|
|
@@ -325,7 +619,7 @@ do_run() {
|
|
|
325
619
|
head_result="$( [ "$head_status" -eq 0 ] && echo GREEN || echo FAIL )"
|
|
326
620
|
if [ "$base_status" -eq 0 ] && [ "$head_status" -eq 0 ]; then verdict=PASS; else verdict=FAIL; exit_code=5; fi
|
|
327
621
|
else
|
|
328
|
-
if [ "$base_status" -ne 0 ] && grep -Eq "$expect" "$trace_dir/repro/$check_id.base.log"; then
|
|
622
|
+
if [ "$base_status" -ne 0 ] && trace_path_safe "$trace_dir/repro/$check_id.base.log" file && grep -Eq -- "$expect" "$trace_dir/repro/$check_id.base.log"; then
|
|
329
623
|
base_result="ERROR"
|
|
330
624
|
if [ "$head_status" -eq 0 ]; then head_result="GREEN"; verdict=PASS; else head_result="FAIL"; verdict=FAIL; exit_code=5; fi
|
|
331
625
|
else base_result="FAIL"; head_result="$( [ "$head_status" -eq 0 ] && echo GREEN || echo FAIL )"; verdict=FAIL; exit_code=5; fi
|
|
@@ -335,6 +629,91 @@ do_run() {
|
|
|
335
629
|
exit "$exit_code"
|
|
336
630
|
}
|
|
337
631
|
|
|
632
|
+
acceptance_rows() {
|
|
633
|
+
local file="$trace_dir/02-reproduction.md"
|
|
634
|
+
trace_path_safe "$file" file || return 1
|
|
635
|
+
awk -F '|' '
|
|
636
|
+
# Acceptance tables deliberately accept LF and CRLF files. Remove only
|
|
637
|
+
# the record terminator CR; an embedded CR remains a rejected control byte.
|
|
638
|
+
{ sub(/\r$/, "", $0) }
|
|
639
|
+
/^## Acceptance checks$/ { in_table = 1; next }
|
|
640
|
+
/^## / { if (in_table) in_table = 0 }
|
|
641
|
+
!in_table { next }
|
|
642
|
+
$0 == "| AC | class | check | argv | expect | pre-fix | post-fix | notes |" { header = 1; next }
|
|
643
|
+
/^\|[-[:space:]|]+\|[[:space:]]*$/ { next }
|
|
644
|
+
/^\|[[:space:]]*AC[0-9]+[[:space:]]*\|/ {
|
|
645
|
+
if (NF != 10) { bad = 1; next }
|
|
646
|
+
for (i = 2; i <= 9; i++) {
|
|
647
|
+
cell = $i
|
|
648
|
+
sub(/^[ \t]+/, "", cell)
|
|
649
|
+
sub(/[ \t]+$/, "", cell)
|
|
650
|
+
if (cell ~ /[[:cntrl:]]/) bad = 1
|
|
651
|
+
cells[i] = cell
|
|
652
|
+
}
|
|
653
|
+
print cells[2] "\t" cells[3] "\t" cells[4] "\t" cells[5] "\t" cells[6]
|
|
654
|
+
count += 1
|
|
655
|
+
next
|
|
656
|
+
}
|
|
657
|
+
/^\|/ { bad = 1 }
|
|
658
|
+
END { if (!header || bad || count == 0) exit 1 }
|
|
659
|
+
' "$file"
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
semantic_digest() {
|
|
663
|
+
local canonical
|
|
664
|
+
canonical="$(acceptance_rows)" || {
|
|
665
|
+
echo "repro-check: acceptance table is missing or malformed" >&2
|
|
666
|
+
return 1
|
|
667
|
+
}
|
|
668
|
+
printf '%s\n' "$canonical" | git hash-object --stdin
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
verify_manifest_semantics() {
|
|
672
|
+
local manifest="$1" rows table_exec manifest_exec manifest_semantics
|
|
673
|
+
trace_path_safe "$manifest" file || return 1
|
|
674
|
+
validate_manifest "$manifest" "" allow-legacy-format-only
|
|
675
|
+
rows="$(acceptance_rows)" || {
|
|
676
|
+
echo "repro-check: acceptance table is missing or malformed" >&2
|
|
677
|
+
return 1
|
|
678
|
+
}
|
|
679
|
+
table_exec="$(printf '%s\n' "$rows" | awk -F '\t' '$2 != "NON-EXECUTABLE" { print $3 "\t" $4 "\t" $5 }' | LC_ALL=C sort)"
|
|
680
|
+
# A single acceptance check may freeze multiple files. The manifest keeps
|
|
681
|
+
# one row per (path, check-id) pair for file-integrity replay, while the
|
|
682
|
+
# acceptance table has one semantic row per check. Collapse the effective
|
|
683
|
+
# manifest by check-id here, but reject a hand-edited manifest that gives one
|
|
684
|
+
# check divergent argv/expect semantics on different paths.
|
|
685
|
+
if ! manifest_semantics="$(awk -F '\t' '
|
|
686
|
+
NR > 1 {
|
|
687
|
+
pair = length($3) ":" $3 ":" $6
|
|
688
|
+
latest_seq[pair] = $1
|
|
689
|
+
latest_check[pair] = $6
|
|
690
|
+
latest_argv[pair] = $7
|
|
691
|
+
latest_expect[pair] = $8
|
|
692
|
+
}
|
|
693
|
+
END {
|
|
694
|
+
for (pair in latest_check) {
|
|
695
|
+
check = latest_check[pair]
|
|
696
|
+
if (seen[check] && (check_argv[check] != latest_argv[pair] || check_expect[check] != latest_expect[pair])) {
|
|
697
|
+
exit 1
|
|
698
|
+
}
|
|
699
|
+
seen[check] = 1
|
|
700
|
+
check_argv[check] = latest_argv[pair]
|
|
701
|
+
check_expect[check] = latest_expect[pair]
|
|
702
|
+
}
|
|
703
|
+
for (check in seen) print check "\t" check_argv[check] "\t" check_expect[check]
|
|
704
|
+
}
|
|
705
|
+
' "$manifest")"; then
|
|
706
|
+
echo "repro-check: checkpoint manifest has divergent semantics for one check" >&2
|
|
707
|
+
return 1
|
|
708
|
+
fi
|
|
709
|
+
manifest_exec="$(printf '%s\n' "$manifest_semantics" | LC_ALL=C sort)"
|
|
710
|
+
if [ "$table_exec" = "$manifest_exec" ]; then
|
|
711
|
+
return 0
|
|
712
|
+
fi
|
|
713
|
+
echo "repro-check: acceptance table semantics do not match checkpoint manifest" >&2
|
|
714
|
+
return 1
|
|
715
|
+
}
|
|
716
|
+
|
|
338
717
|
do_checkpoint() {
|
|
339
718
|
local reason="-" check_id="" argv="" expect="" base="" path manifest seq kind mode blob row
|
|
340
719
|
trace_dir=""; slug=""
|
|
@@ -348,42 +727,66 @@ do_checkpoint() {
|
|
|
348
727
|
done
|
|
349
728
|
[ "$#" -gt 0 ] || usage
|
|
350
729
|
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|
|
|
730
|
+
case "$reason" in -) kind=CHECKPOINT;; CHECK_WRONG|AC_CHANGED_BY_USER) kind=AMEND;; *) echo "repro-check: invalid amendment reason (use CHECK_WRONG or AC_CHANGED_BY_USER)" >&2; exit 2;; esac
|
|
352
731
|
case "$base" in ''|-*) echo "repro-check: --base must name a commit" >&2; exit 2;; esac
|
|
353
732
|
git rev-parse --verify --quiet "$base^{commit}" >/dev/null || { echo "repro-check: --base does not resolve to a commit" >&2; exit 2; }
|
|
354
|
-
if
|
|
355
|
-
echo "repro-check: manifest fields cannot contain
|
|
733
|
+
if has_bad_control "$argv" || has_bad_control "$expect"; then
|
|
734
|
+
echo "repro-check: manifest fields cannot contain control bytes" >&2
|
|
356
735
|
exit 2
|
|
357
736
|
fi
|
|
358
|
-
|
|
737
|
+
trace_for
|
|
359
738
|
require_contained "$trace_dir"
|
|
360
|
-
require_contained "$
|
|
361
|
-
|
|
362
|
-
|
|
739
|
+
require_contained "$trace_dir/repro"
|
|
740
|
+
refuse_ancestor_symlink_escape "$trace_dir/repro"
|
|
741
|
+
mkdir -p "$trace_dir/repro"
|
|
742
|
+
require_contained "$trace_dir/repro"
|
|
743
|
+
set_trace_root
|
|
744
|
+
manifest="$trace_dir/repro/checkpoint.manifest"
|
|
745
|
+
trace_path_safe "$trace_dir/repro" dir || { echo "repro-check: repro directory is missing or unsafe" >&2; exit 2; }
|
|
746
|
+
trace_path_safe "$manifest" file || true
|
|
363
747
|
refuse_nonregular_target "$manifest"
|
|
364
|
-
[ -f "$manifest" ]
|
|
748
|
+
if [ ! -f "$manifest" ]; then
|
|
749
|
+
open_exclusive_target "$manifest" || { echo "repro-check: could not create checkpoint manifest safely" >&2; exit 2; }
|
|
750
|
+
printf '# issue-tracer checkpoint manifest v1 rows=0\n' >&"$exclusive_fd"
|
|
751
|
+
close_exclusive_target
|
|
752
|
+
fi
|
|
365
753
|
# validate_manifest owns the header check: it is strictly stronger than the
|
|
366
754
|
# old `grep -Fx` (which matched the string on ANY line) and additionally
|
|
367
755
|
# proves the recorded count, the seq run, and the field count.
|
|
368
|
-
|
|
756
|
+
if [ "$kind" = AMEND ]; then
|
|
757
|
+
# An amendment may be the next step in reconciling two same-path checks
|
|
758
|
+
# that captured different bytes. Strict verification still rejects the
|
|
759
|
+
# intermediate manifest; permit only this targeted append to proceed. The
|
|
760
|
+
# legacy FORMAT_ONLY exception is validation-only: the reason parser above
|
|
761
|
+
# still rejects a newly requested FORMAT_ONLY amendment.
|
|
762
|
+
validate_manifest "$manifest" allow-conflicts allow-legacy-format-only
|
|
763
|
+
else
|
|
764
|
+
validate_manifest "$manifest"
|
|
765
|
+
fi
|
|
369
766
|
seq="$(awk 'END {print NR - 1}' "$manifest")"
|
|
370
767
|
for path in "$@"; do
|
|
371
768
|
is_inside_root "$path" || { echo "repro-check: checkpoint path must be repo-relative without ..: $path" >&2; exit 2; }
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
# Re-running the sanctioned `checkpoint` command on an already-frozen
|
|
769
|
+
has_bad_path "$path" && { echo "repro-check: checkpoint path cannot contain control bytes" >&2; exit 2; }
|
|
770
|
+
repo_file_safe "$path" || { echo "repro-check: checkpoint path must be a regular in-repository file: $path" >&2; exit 2; }
|
|
771
|
+
# Re-running the sanctioned `checkpoint` command on an already-frozen pair
|
|
375
772
|
# would append a fresh CHECKPOINT row that last-writer-wins re-baselines a
|
|
376
773
|
# weakened check to green in do_verify. Refuse it: superseding a frozen
|
|
377
|
-
#
|
|
378
|
-
# The manifest is re-read per path, so a
|
|
774
|
+
# pair requires an AMEND row that names a reason and stays in the file.
|
|
775
|
+
# The manifest is re-read per path, so a pair frozen by an earlier
|
|
379
776
|
# iteration of this same invocation is already recorded and also refused.
|
|
380
|
-
if [ "$kind" = CHECKPOINT ] &&
|
|
381
|
-
echo "repro-check: $path is already frozen; supersede it with --reason CHECK_WRONG|
|
|
777
|
+
if [ "$kind" = CHECKPOINT ] && manifest_has_pair "$manifest" "$path" "$check_id"; then
|
|
778
|
+
echo "repro-check: $path ($check_id) is already frozen; supersede it with --reason CHECK_WRONG|AC_CHANGED_BY_USER" >&2
|
|
779
|
+
exit 2
|
|
780
|
+
fi
|
|
781
|
+
if [ "$kind" = AMEND ] && ! manifest_has_pair "$manifest" "$path" "$check_id"; then
|
|
782
|
+
echo "repro-check: $path ($check_id) cannot be amended before it is checkpointed" >&2
|
|
382
783
|
exit 2
|
|
383
784
|
fi
|
|
384
|
-
|
|
785
|
+
# Always capture the current bytes for a new pair. Do not inherit the blob
|
|
786
|
+
# from another check that happens to use the same path.
|
|
787
|
+
blob="$(git hash-object "$root_real/$path")"
|
|
385
788
|
mode="$(git ls-files -s -- "$path" | awk 'NR==1 {print $1}')"
|
|
386
|
-
[ -n "$mode" ] || { [ -x "$
|
|
789
|
+
[ -n "$mode" ] || { [ -x "$root_real/$path" ] && mode=100755 || mode=100644; }
|
|
387
790
|
seq=$((seq + 1))
|
|
388
791
|
# $seq is post-increment, so it is also the new total row count that the
|
|
389
792
|
# header must record. Row and header land together in one temp-file swap.
|
|
@@ -394,23 +797,147 @@ do_checkpoint() {
|
|
|
394
797
|
}
|
|
395
798
|
|
|
396
799
|
do_verify() {
|
|
397
|
-
local manifest line path old new changed=0
|
|
800
|
+
local manifest line path old check_id new changed=0
|
|
398
801
|
trace_dir=""; slug=""
|
|
399
802
|
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
803
|
valid_slug "$slug" || { echo "repro-check: invalid slug" >&2; exit 2; }
|
|
401
|
-
|
|
402
|
-
|
|
804
|
+
trace_for
|
|
805
|
+
set_trace_root
|
|
806
|
+
manifest="$trace_dir/repro/checkpoint.manifest"
|
|
807
|
+
trace_path_safe "$manifest" file || { echo "repro-check: checkpoint manifest missing or invalid" >&2; exit 2; }
|
|
403
808
|
# Iterating only the surviving rows would silently drop a frozen check when a
|
|
404
809
|
# row is deleted OR the tail is truncated, so structure - header count, seq
|
|
405
810
|
# run, field count - is proven before any row is replayed.
|
|
406
|
-
validate_manifest "$manifest"
|
|
407
|
-
while IFS=$'\t' read -r path old; do
|
|
408
|
-
[ -
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
811
|
+
validate_manifest "$manifest" "" allow-legacy-format-only
|
|
812
|
+
while IFS=$'\t' read -r path old check_id; do
|
|
813
|
+
if [ -z "$path" ] || ! is_inside_root "$path" || ! valid_id "$check_id"; then
|
|
814
|
+
echo "repro-check: checkpoint manifest contains an unsafe path or invalid check id" >&2
|
|
815
|
+
exit 2
|
|
816
|
+
fi
|
|
817
|
+
if ! repo_file_safe "$path"; then
|
|
818
|
+
echo "CHANGED $path $old MISSING"; changed=1; continue
|
|
819
|
+
fi
|
|
820
|
+
new="$(git hash-object "$root_real/$path")"
|
|
821
|
+
if [ "$old" = "$new" ]; then echo "OK $path ($check_id)"; else echo "CHANGED $path $old $new ($check_id)"; changed=1; fi
|
|
822
|
+
done < <(awk -F '\t' '
|
|
823
|
+
NR > 1 {
|
|
824
|
+
pair = length($3) ":" $3 ":" $6
|
|
825
|
+
latest_seq[pair] = $1
|
|
826
|
+
latest_path[pair] = $3
|
|
827
|
+
latest_blob[pair] = $4
|
|
828
|
+
latest_check[pair] = $6
|
|
829
|
+
}
|
|
830
|
+
END {
|
|
831
|
+
for (pair in latest_path) print latest_seq[pair] "\t" latest_path[pair] "\t" latest_blob[pair] "\t" latest_check[pair]
|
|
832
|
+
}
|
|
833
|
+
' "$manifest" | sort -n -k1,1 | cut -f2-)
|
|
412
834
|
[ "$changed" -eq 0 ] || exit 1
|
|
413
835
|
}
|
|
414
836
|
|
|
837
|
+
do_verify_semantics() {
|
|
838
|
+
local manifest
|
|
839
|
+
trace_dir=""; slug=""
|
|
840
|
+
while [ "$#" -gt 0 ]; do
|
|
841
|
+
case "$1" in
|
|
842
|
+
--slug) [ "$#" -ge 2 ] || usage; slug="$2"; shift 2;;
|
|
843
|
+
--trace-dir) [ "$#" -ge 2 ] || usage; trace_dir="$2"; shift 2;;
|
|
844
|
+
*) usage;;
|
|
845
|
+
esac
|
|
846
|
+
done
|
|
847
|
+
valid_slug "$slug" || { echo "repro-check: invalid slug" >&2; exit 2; }
|
|
848
|
+
trace_for
|
|
849
|
+
set_trace_root
|
|
850
|
+
manifest="$trace_dir/repro/checkpoint.manifest"
|
|
851
|
+
trace_path_safe "$manifest" file || { echo "repro-check: checkpoint manifest missing or invalid" >&2; exit 2; }
|
|
852
|
+
verify_manifest_semantics "$manifest"
|
|
853
|
+
echo "semantics: OK"
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
state_value() {
|
|
857
|
+
local key="$1" state_file="$trace_dir/state.md"
|
|
858
|
+
[ -n "$trace_dir" ] || state_file="$root/.agents/issue-traces/$slug/state.md"
|
|
859
|
+
state_file="$(to_shell_path "$state_file")"
|
|
860
|
+
trace_path_safe "$state_file" file || return 0
|
|
861
|
+
awk -F ': ' -v key="$key" '$1 == key { print substr($0, length(key) + 3); exit }' "$state_file" 2>/dev/null || true
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
receipt_for() {
|
|
865
|
+
local manifest="$1" digest semantics tree
|
|
866
|
+
trace_path_safe "$manifest" file || { echo "repro-check: checkpoint manifest missing or unsafe" >&2; exit 2; }
|
|
867
|
+
digest="$(git hash-object --no-filters "$manifest")"
|
|
868
|
+
semantics="$(semantic_digest)" || exit 2
|
|
869
|
+
tree="$(state_value checkpoint-tree-id)"
|
|
870
|
+
is_sha1 "$tree" || { echo "repro-check: state.md has no valid checkpoint-tree-id" >&2; exit 2; }
|
|
871
|
+
printf 'issue-tracer-checkpoint-v1 slug=%s manifest=%s semantics=%s tree=%s\n' "$slug" "$digest" "$semantics" "$tree"
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
do_anchor() {
|
|
875
|
+
local manifest
|
|
876
|
+
trace_dir=""; slug=""
|
|
877
|
+
while [ "$#" -gt 0 ]; do
|
|
878
|
+
case "$1" in
|
|
879
|
+
--slug) [ "$#" -ge 2 ] || usage; slug="$2"; shift 2;;
|
|
880
|
+
--trace-dir) [ "$#" -ge 2 ] || usage; trace_dir="$2"; shift 2;;
|
|
881
|
+
*) usage;;
|
|
882
|
+
esac
|
|
883
|
+
done
|
|
884
|
+
valid_slug "$slug" || { echo "repro-check: invalid slug" >&2; exit 2; }
|
|
885
|
+
trace_for
|
|
886
|
+
set_trace_root
|
|
887
|
+
manifest="$trace_dir/repro/checkpoint.manifest"
|
|
888
|
+
trace_path_safe "$manifest" file || { echo "repro-check: checkpoint manifest missing or invalid" >&2; exit 2; }
|
|
889
|
+
# Verification diagnostics are intentionally kept on stderr by do_verify;
|
|
890
|
+
# anchor's stdout is a machine-readable, single-line receipt for reviewers
|
|
891
|
+
# to copy without filtering replay output.
|
|
892
|
+
do_verify --slug "$slug" --trace-dir "$trace_dir" >/dev/null
|
|
893
|
+
verify_manifest_semantics "$manifest" >/dev/null
|
|
894
|
+
receipt_for "$manifest"
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
do_verify_anchor() {
|
|
898
|
+
local receipt="" manifest expected_digest expected_semantics expected_tree parsed_slug parsed_manifest parsed_semantics parsed_tree
|
|
899
|
+
trace_dir=""; slug=""
|
|
900
|
+
while [ "$#" -gt 0 ]; do
|
|
901
|
+
case "$1" in
|
|
902
|
+
--slug) [ "$#" -ge 2 ] || usage; slug="$2"; shift 2;;
|
|
903
|
+
--trace-dir) [ "$#" -ge 2 ] || usage; trace_dir="$2"; shift 2;;
|
|
904
|
+
--receipt) [ "$#" -ge 2 ] || usage; receipt="$2"; shift 2;;
|
|
905
|
+
*) usage;;
|
|
906
|
+
esac
|
|
907
|
+
done
|
|
908
|
+
valid_slug "$slug" || { echo "repro-check: invalid slug" >&2; exit 2; }
|
|
909
|
+
case "$receipt" in *$'\n'*|*$'\r'*|*$'\t'*) echo "repro-check: anchor receipt must be one single line" >&2; exit 2;; esac
|
|
910
|
+
if ! printf '%s\n' "$receipt" | grep -Eq '^issue-tracer-checkpoint-v1 slug=[a-z0-9-]+ manifest=[0-9a-f]{40} semantics=[0-9a-f]{40} tree=[0-9a-f]{40}$'; then
|
|
911
|
+
echo "repro-check: malformed anchor receipt" >&2
|
|
912
|
+
exit 2
|
|
913
|
+
fi
|
|
914
|
+
parsed_slug="${receipt#*slug=}"; parsed_slug="${parsed_slug%% manifest=*}"
|
|
915
|
+
parsed_manifest="${receipt#*manifest=}"; parsed_manifest="${parsed_manifest%% semantics=*}"
|
|
916
|
+
parsed_semantics="${receipt#*semantics=}"; parsed_semantics="${parsed_semantics%% tree=*}"
|
|
917
|
+
parsed_tree="${receipt##* tree=}"
|
|
918
|
+
[ "$parsed_slug" = "$slug" ] || { echo "repro-check: anchor receipt slug does not match --slug" >&2; exit 2; }
|
|
919
|
+
trace_for
|
|
920
|
+
set_trace_root
|
|
921
|
+
manifest="$trace_dir/repro/checkpoint.manifest"
|
|
922
|
+
trace_path_safe "$manifest" file || { echo "repro-check: checkpoint manifest missing or invalid" >&2; exit 2; }
|
|
923
|
+
do_verify --slug "$slug" --trace-dir "$trace_dir"
|
|
924
|
+
trace_path_safe "$manifest" file || { echo "repro-check: checkpoint manifest became unsafe" >&2; exit 2; }
|
|
925
|
+
verify_manifest_semantics "$manifest" >/dev/null
|
|
926
|
+
expected_digest="$(git hash-object --no-filters "$manifest")"
|
|
927
|
+
expected_semantics="$(semantic_digest)"
|
|
928
|
+
expected_tree="$(state_value checkpoint-tree-id)"
|
|
929
|
+
[ "$expected_digest" = "$parsed_manifest" ] || { echo "repro-check: anchor manifest digest does not match checkpoint manifest" >&2; exit 1; }
|
|
930
|
+
[ "$expected_semantics" = "$parsed_semantics" ] || { echo "repro-check: anchor semantic digest does not match acceptance table" >&2; exit 1; }
|
|
931
|
+
[ "$expected_tree" = "$parsed_tree" ] || { echo "repro-check: anchor tree does not match state.md checkpoint-tree-id" >&2; exit 1; }
|
|
932
|
+
}
|
|
933
|
+
|
|
415
934
|
command="${1:-}"; shift || true
|
|
416
|
-
case "$command" in
|
|
935
|
+
case "$command" in
|
|
936
|
+
run) do_run "$@";;
|
|
937
|
+
checkpoint) do_checkpoint "$@";;
|
|
938
|
+
verify-checkpoint) do_verify "$@";;
|
|
939
|
+
verify-semantics) do_verify_semantics "$@";;
|
|
940
|
+
anchor) do_anchor "$@";;
|
|
941
|
+
verify-anchor) do_verify_anchor "$@";;
|
|
942
|
+
*) usage;;
|
|
943
|
+
esac
|