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
|
@@ -14,13 +14,148 @@ to_shell_path() {
|
|
|
14
14
|
|
|
15
15
|
root="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "trace-check: not inside a git work tree" >&2; exit 2; }
|
|
16
16
|
root="$(to_shell_path "$root")"
|
|
17
|
+
root_real="$(cd "$root" && pwd -P)"
|
|
18
|
+
issue_traces_base="$root_real/.agents/issue-traces"
|
|
17
19
|
script_dir="$(cd "$(dirname "$0")" && pwd -P)"
|
|
18
20
|
failed=0
|
|
19
21
|
legacy=0
|
|
22
|
+
trace_root_real=""
|
|
20
23
|
|
|
21
24
|
usage() { echo "usage: trace-check.sh {tree-id|handshake|phase <phase> --slug <slug> [--trace-dir <dir>]|merge --slug <slug>}" >&2; exit 2; }
|
|
22
25
|
valid_slug() { case "$1" in ''|*[!a-z0-9-]*) return 1;; *) return 0;; esac; }
|
|
23
26
|
trim() { printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; }
|
|
27
|
+
has_bad_control() {
|
|
28
|
+
# Shell variables can carry newlines, but grep treats them as record
|
|
29
|
+
# separators; reject those explicitly before scanning the remaining bytes.
|
|
30
|
+
case "$1" in *$'\t'*|*$'\n'*|*$'\r'*) return 0;; esac
|
|
31
|
+
local matches
|
|
32
|
+
# grep -c drains stdin. A terminal grep -q can make printf receive SIGPIPE
|
|
33
|
+
# under inherited pipefail, turning a real control byte into a false miss.
|
|
34
|
+
matches="$(LC_ALL=C printf '%s' "$1" | LC_ALL=C grep -c '[[:cntrl:]]' || true)"
|
|
35
|
+
[ "${matches:-0}" -gt 0 ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Markdown trace artifacts are commonly authored on Windows. Keep every
|
|
39
|
+
# line-oriented parser below independent of the file's record separator while
|
|
40
|
+
# preserving embedded control bytes for the explicit validation paths.
|
|
41
|
+
normalize_terminal_cr() { sed 's/\r$//' "$1"; }
|
|
42
|
+
|
|
43
|
+
# Validate the caller-selected trace directory before opening state.md. The
|
|
44
|
+
# lexical prefix check rejects absolute escapes and dot components before any
|
|
45
|
+
# canonicalization (which would otherwise turn an escaped path back into an
|
|
46
|
+
# apparently safe one). The deepest existing ancestor and each existing
|
|
47
|
+
# component are then checked for symlinks/junctions so a path that looks inside
|
|
48
|
+
# the root cannot redirect reads outside it.
|
|
49
|
+
validate_trace_dir() {
|
|
50
|
+
local candidate="$1" check parent resolved canonical_candidate
|
|
51
|
+
has_bad_control "$candidate" && { echo "trace-check: --trace-dir cannot contain control bytes" >&2; exit 2; }
|
|
52
|
+
candidate="$(to_shell_path "$candidate")"
|
|
53
|
+
has_bad_control "$candidate" && { echo "trace-check: --trace-dir cannot contain control bytes" >&2; exit 2; }
|
|
54
|
+
case "$candidate/" in
|
|
55
|
+
*/../*|*/./*|*\\*) echo "trace-check: --trace-dir cannot contain . or .. components or backslashes" >&2; exit 2;;
|
|
56
|
+
esac
|
|
57
|
+
# Inspect the caller's spelling before the alias fallback canonicalizes it.
|
|
58
|
+
# Otherwise a symlink/junction that resolves back inside the trace root would
|
|
59
|
+
# disappear from the later component walk and be accepted as a safe path.
|
|
60
|
+
check="$candidate"
|
|
61
|
+
while :; do
|
|
62
|
+
if [ -L "$check" ]; then
|
|
63
|
+
echo "trace-check: refusing symlinked trace component: $check" >&2
|
|
64
|
+
exit 2
|
|
65
|
+
fi
|
|
66
|
+
[ "$check" = "/" ] && break
|
|
67
|
+
parent="$(dirname "$check")"
|
|
68
|
+
[ "$parent" != "$check" ] || break
|
|
69
|
+
check="$parent"
|
|
70
|
+
done
|
|
71
|
+
case "$candidate/" in
|
|
72
|
+
"$root_real/.agents/issue-traces/"*) ;;
|
|
73
|
+
*)
|
|
74
|
+
# Windows may preserve an 8.3 alias (for example RUNNER~1) in the
|
|
75
|
+
# caller's absolute path while Git resolves the repository through its
|
|
76
|
+
# long spelling. Resolve an existing explicit directory before rejecting
|
|
77
|
+
# it, while retaining the lexical traversal/backslash rejection above.
|
|
78
|
+
canonical_candidate="$(cd "$candidate" 2>/dev/null && pwd -P)" || {
|
|
79
|
+
echo "trace-check: --trace-dir must be inside .agents/issue-traces" >&2
|
|
80
|
+
exit 2
|
|
81
|
+
}
|
|
82
|
+
case "$canonical_candidate/" in
|
|
83
|
+
"$root_real/.agents/issue-traces/"*) candidate="$canonical_candidate" ;;
|
|
84
|
+
*) echo "trace-check: --trace-dir must be inside .agents/issue-traces" >&2; exit 2;;
|
|
85
|
+
esac
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
|
|
89
|
+
check="$candidate"
|
|
90
|
+
while [ -L "$check" ]; do
|
|
91
|
+
echo "trace-check: refusing symlinked trace path: $candidate" >&2
|
|
92
|
+
exit 2
|
|
93
|
+
done
|
|
94
|
+
while [ ! -e "$check" ]; do
|
|
95
|
+
parent="$(dirname "$check")"
|
|
96
|
+
[ "$parent" != "$check" ] || break
|
|
97
|
+
check="$parent"
|
|
98
|
+
[ -L "$check" ] || continue
|
|
99
|
+
echo "trace-check: refusing symlinked trace ancestor: $check" >&2
|
|
100
|
+
exit 2
|
|
101
|
+
done
|
|
102
|
+
[ -e "$check" ] || { echo "trace-check: could not resolve trace path: $candidate" >&2; exit 2; }
|
|
103
|
+
resolved="$(cd "$check" 2>/dev/null && pwd -P)" || { echo "trace-check: could not resolve trace path: $candidate" >&2; exit 2; }
|
|
104
|
+
case "$resolved/" in
|
|
105
|
+
"$root_real/"*) ;;
|
|
106
|
+
*) echo "trace-check: --trace-dir resolves outside the project root" >&2; exit 2;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
109
|
+
check="$candidate"
|
|
110
|
+
while [ "$check" != "$root_real" ] && [ "$check" != "/" ]; do
|
|
111
|
+
if [ -L "$check" ]; then
|
|
112
|
+
echo "trace-check: refusing symlinked trace component: $check" >&2
|
|
113
|
+
exit 2
|
|
114
|
+
fi
|
|
115
|
+
check="$(dirname "$check")"
|
|
116
|
+
done
|
|
117
|
+
[ "$check" = "$root_real" ] || { echo "trace-check: --trace-dir is not rooted at the project" >&2; exit 2; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Validate a path below the already-canonical trace root immediately before it
|
|
121
|
+
# is read. `[ -f ]` follows symlinks, so it cannot be used as the first check:
|
|
122
|
+
# a leaf or an intermediate directory could redirect an otherwise in-root
|
|
123
|
+
# artifact to an arbitrary outside file. The ancestor walk catches both POSIX
|
|
124
|
+
# symlinks and Windows junctions as reported by MSYS `test -L`; the canonical
|
|
125
|
+
# parent check is defense in depth for filesystem races and unusual link forms.
|
|
126
|
+
trace_path_safe() {
|
|
127
|
+
local path="$1" kind="${2:-file}" ancestor parent resolved
|
|
128
|
+
[ -n "$trace_root_real" ] || { echo "trace-check: trace root is not initialized" >&2; exit 2; }
|
|
129
|
+
case "$path/" in
|
|
130
|
+
"$trace_root_real/"*) ;;
|
|
131
|
+
*) echo "trace-check: refusing path outside canonical trace root: $path" >&2; exit 2;;
|
|
132
|
+
esac
|
|
133
|
+
case "$path/" in
|
|
134
|
+
*/../*|*/./*|*\\*) echo "trace-check: refusing ambiguous trace path: $path" >&2; exit 2;;
|
|
135
|
+
esac
|
|
136
|
+
ancestor="$path"
|
|
137
|
+
while [ "$ancestor" != "$trace_root_real" ] && [ "$ancestor" != "/" ]; do
|
|
138
|
+
if [ -L "$ancestor" ]; then
|
|
139
|
+
echo "trace-check: refusing symlinked trace component: $ancestor" >&2
|
|
140
|
+
exit 2
|
|
141
|
+
fi
|
|
142
|
+
ancestor="$(dirname "$ancestor")"
|
|
143
|
+
done
|
|
144
|
+
[ "$ancestor" = "$trace_root_real" ] || { echo "trace-check: trace path is not rooted at the canonical trace directory: $path" >&2; exit 2; }
|
|
145
|
+
[ "$path" = "$trace_root_real" ] && { [ "$kind" = dir ] && [ -d "$path" ]; return $?; }
|
|
146
|
+
parent="$(dirname "$path")"
|
|
147
|
+
[ -d "$parent" ] || return 1
|
|
148
|
+
resolved="$(cd "$parent" 2>/dev/null && pwd -P)" || { echo "trace-check: could not resolve trace artifact parent: $path" >&2; exit 2; }
|
|
149
|
+
case "$resolved/" in
|
|
150
|
+
"$trace_root_real/"*) ;;
|
|
151
|
+
*) echo "trace-check: trace artifact parent resolves outside canonical trace root: $path" >&2; exit 2;;
|
|
152
|
+
esac
|
|
153
|
+
case "$kind" in
|
|
154
|
+
file) [ -f "$path" ] || return 1;;
|
|
155
|
+
dir) [ -d "$path" ] || return 1;;
|
|
156
|
+
*) echo "trace-check: internal invalid trace path kind: $kind" >&2; exit 2;;
|
|
157
|
+
esac
|
|
158
|
+
}
|
|
24
159
|
|
|
25
160
|
tree_id() {
|
|
26
161
|
# Trace artifacts under .agents/issue-traces/ must never affect this
|
|
@@ -46,14 +181,14 @@ tree_id() {
|
|
|
46
181
|
|
|
47
182
|
handshake() {
|
|
48
183
|
local version candidate value shim verdict worst="MATCH"
|
|
49
|
-
version="$(awk '/^metadata:/{in_metadata=1; next} in_metadata && /^ version:/{sub(/^ version:[[:space:]]*/, ""); print; exit}' "$root/.opencode/skills/issue-tracer/SKILL.md" 2>/dev/null || true)"
|
|
184
|
+
version="$(awk '{ sub(/\r$/, "", $0) } /^metadata:/{in_metadata=1; next} in_metadata && /^ version:/{sub(/^ version:[[:space:]]*/, ""); print; exit}' "$root/.opencode/skills/issue-tracer/SKILL.md" 2>/dev/null || true)"
|
|
50
185
|
[ -n "$version" ] || version="unknown"
|
|
51
186
|
for candidate in "${HOME:-}/.claude/skills/issue-tracer/SKILL.md" "${HOME:-}/.codex/skills/issue-tracer/SKILL.md" "${HOME:-}/.agents/skills/issue-tracer/SKILL.md" "${HOME:-}/.zcode/skills/issue-tracer/SKILL.md"; do
|
|
52
187
|
if [ ! -f "$candidate" ]; then
|
|
53
188
|
verdict="ABSENT"
|
|
54
189
|
else
|
|
55
|
-
value="$(
|
|
56
|
-
shim="$(
|
|
190
|
+
value="$(normalize_terminal_cr "$candidate" 2>/dev/null | grep -m1 '^ version:' | sed 's/^ version:[[:space:]]*//' || true)"
|
|
191
|
+
shim="$(normalize_terminal_cr "$candidate" 2>/dev/null | grep -m1 '^shim:' | sed 's/^shim:[[:space:]]*//' || true)"
|
|
57
192
|
if [ "$value" = "$version" ] && [ "$shim" = "true" ]; then verdict="SHIM"
|
|
58
193
|
elif [ "$value" = "$version" ]; then verdict="MATCH"
|
|
59
194
|
else verdict="STALE:$candidate"; fi
|
|
@@ -72,21 +207,28 @@ rule_ok() { echo "OK $1"; }
|
|
|
72
207
|
rule_bad() {
|
|
73
208
|
if [ "$legacy" -eq 1 ]; then echo "WARN $1: $2"; else echo "FAIL $1: $2"; failed=1; fi
|
|
74
209
|
}
|
|
75
|
-
|
|
210
|
+
state_lines() {
|
|
211
|
+
trace_path_safe "$state" file || return 0
|
|
212
|
+
normalize_terminal_cr "$state"
|
|
213
|
+
}
|
|
214
|
+
state_value() {
|
|
215
|
+
trace_path_safe "$state" file || return 0
|
|
216
|
+
state_lines | awk -F ': ' -v key="$1" '$1 == key { print substr($0, length(key) + 3); exit }' 2>/dev/null || true
|
|
217
|
+
}
|
|
76
218
|
is_hex() { case "$1" in [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) return 0;; *) return 1;; esac; }
|
|
77
219
|
|
|
78
220
|
check_headings() {
|
|
79
221
|
local file="$1"; shift
|
|
80
|
-
if
|
|
222
|
+
if ! trace_path_safe "$file" file; then rule_bad "artifact-$(basename "$file")" "missing"; return; fi
|
|
81
223
|
local heading count
|
|
82
224
|
for heading in "$@"; do
|
|
83
|
-
count="$(
|
|
225
|
+
count="$(awk -v want="$heading" '{ sub(/\r$/, "", $0); if ($0 == want) n++ } END { print n + 0 }' "$file" 2>/dev/null)"
|
|
84
226
|
if [ "$count" -eq 1 ]; then rule_ok "heading-${heading#\#\# }"
|
|
85
227
|
elif [ "$count" -gt 1 ]; then rule_bad "duplicate-heading-${heading#\#\# }" "in $(basename "$file")"
|
|
86
228
|
else rule_bad "heading-${heading#\#\# }" "missing in $(basename "$file")"; fi
|
|
87
229
|
done
|
|
88
230
|
# Any duplicated level-two heading is invalid even when it is not required.
|
|
89
|
-
while IFS= read -r heading; do rule_bad "duplicate-heading-${heading#\#\# }" "in $(basename "$file")"; done < <(
|
|
231
|
+
while IFS= read -r heading; do rule_bad "duplicate-heading-${heading#\#\# }" "in $(basename "$file")"; done < <(awk '{ sub(/\r$/, "", $0); if ($0 ~ /^## /) print }' "$file" 2>/dev/null | sort | uniq -d)
|
|
90
232
|
}
|
|
91
233
|
|
|
92
234
|
# Parse the `## Gates` table row-by-row (split on '|', trim each cell) and
|
|
@@ -97,6 +239,7 @@ check_headings() {
|
|
|
97
239
|
# "DISAPPROVED"; matching is done cell-by-cell after trimming instead.
|
|
98
240
|
gate_row_exists() {
|
|
99
241
|
local gate="$1" verdict_want="$2" commit="$3" treeid="$4" line g v c t
|
|
242
|
+
trace_path_safe "$state" file || return 1
|
|
100
243
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
101
244
|
case "$line" in '|'*) ;; *) continue;; esac
|
|
102
245
|
IFS='|' read -r _ g v c t _ <<EOF
|
|
@@ -108,7 +251,7 @@ EOF
|
|
|
108
251
|
[ -z "$commit" ] || [ "$c" = "$commit" ] || continue
|
|
109
252
|
[ -z "$treeid" ] || [ "$t" = "$treeid" ] || continue
|
|
110
253
|
return 0
|
|
111
|
-
done <
|
|
254
|
+
done < <(state_lines)
|
|
112
255
|
return 1
|
|
113
256
|
}
|
|
114
257
|
|
|
@@ -127,8 +270,9 @@ state_gate() {
|
|
|
127
270
|
# APPROVE.
|
|
128
271
|
artifact_verdict_approved() {
|
|
129
272
|
local file="$1"
|
|
130
|
-
|
|
273
|
+
trace_path_safe "$file" file || return 1
|
|
131
274
|
awk '
|
|
275
|
+
{ sub(/\r$/, "", $0) }
|
|
132
276
|
/^## Verdict$/ { infield = 1; next }
|
|
133
277
|
/^## / { infield = 0 }
|
|
134
278
|
infield {
|
|
@@ -158,7 +302,9 @@ artifact_verdict_approved() {
|
|
|
158
302
|
# callers can distinguish "0 matches" from "duplicate matches").
|
|
159
303
|
artifact_identity() {
|
|
160
304
|
local file="$1" section
|
|
305
|
+
trace_path_safe "$file" file || return 1
|
|
161
306
|
section="$(awk '
|
|
307
|
+
{ sub(/\r$/, "", $0) }
|
|
162
308
|
/^## Reviewed SHA \/ diff hash$/ { infield = 1; next }
|
|
163
309
|
/^## / { infield = 0 }
|
|
164
310
|
infield { print }
|
|
@@ -205,7 +351,9 @@ artifact_identity_matches_gate() {
|
|
|
205
351
|
|
|
206
352
|
recurrence_justification_ok() {
|
|
207
353
|
local file="$1"
|
|
354
|
+
trace_path_safe "$file" file || return 1
|
|
208
355
|
awk '
|
|
356
|
+
{ sub(/\r$/, "", $0) }
|
|
209
357
|
/^## Justification$/ { infield = 1; next }
|
|
210
358
|
/^## / { infield = 0 }
|
|
211
359
|
infield {
|
|
@@ -227,11 +375,12 @@ clean_tree() {
|
|
|
227
375
|
|
|
228
376
|
phase0() {
|
|
229
377
|
local keys key expected actual previous=0 line fresh
|
|
378
|
+
trace_path_safe "$state" file || return
|
|
230
379
|
keys='protocol phase tier classification base-ref base-sha freshness phase0-tree-id checkpoint-tree-id handshake tools merge next-action'
|
|
231
380
|
for key in $keys; do
|
|
232
|
-
line="$(grep -n "^$key: "
|
|
381
|
+
line="$(state_lines | grep -n "^$key: " | cut -d: -f1 | head -n1 || true)"
|
|
233
382
|
if [ -z "$line" ]; then rule_bad "state-$key" "missing"; continue; fi
|
|
234
|
-
if [ "$(grep -c "^$key: "
|
|
383
|
+
if [ "$(state_lines | grep -c "^$key: " || true)" -ne 1 ]; then rule_bad "state-$key" "must appear once"; fi
|
|
235
384
|
if [ "$line" -le "$previous" ]; then rule_bad "state-order" "$key is out of order"; fi
|
|
236
385
|
previous="$line"
|
|
237
386
|
done
|
|
@@ -273,67 +422,177 @@ phase0() {
|
|
|
273
422
|
[ "$(state_value handshake)" != "unset" ] && [ -n "$(state_value handshake)" ] && rule_ok handshake || rule_bad handshake "must be recorded"
|
|
274
423
|
}
|
|
275
424
|
|
|
276
|
-
acceptance_ids() {
|
|
425
|
+
acceptance_ids() {
|
|
426
|
+
local file="$trace/01-issue-summary.md"
|
|
427
|
+
sed 's/\r$//' "$file" 2>/dev/null | grep -E '^- \[[ x]\] AC[0-9]+:' | sed -E 's/^- \[[ x]\] (AC[0-9]+):.*/\1/'
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Return normalized acceptance-table rows as tab-separated cells. The table is
|
|
431
|
+
# intentionally tolerant of LF or CRLF line endings, but never of an embedded
|
|
432
|
+
# control byte: strip only the record-ending CR before validating cells. Shape
|
|
433
|
+
# and control validation happen in this complete pass before phase25 uses AC,
|
|
434
|
+
# check, argv, expect, or notes in rule names/messages.
|
|
435
|
+
acceptance_table_header() {
|
|
436
|
+
awk '
|
|
437
|
+
{ sub(/\r$/, "", $0) }
|
|
438
|
+
/^## Acceptance checks$/ { in_table = 1; next }
|
|
439
|
+
/^## / { if (in_table) in_table = 0 }
|
|
440
|
+
in_table && $0 == "| AC | class | check | argv | expect | pre-fix | post-fix | notes |" { found = 1 }
|
|
441
|
+
END { exit !found }
|
|
442
|
+
' "$1"
|
|
443
|
+
}
|
|
444
|
+
acceptance_table_rows() {
|
|
445
|
+
awk -F '|' '
|
|
446
|
+
{ sub(/\r$/, "", $0) }
|
|
447
|
+
/^## Acceptance checks$/ { in_table = 1; next }
|
|
448
|
+
/^## / { if (in_table) in_table = 0 }
|
|
449
|
+
!in_table { next }
|
|
450
|
+
$0 == "| AC | class | check | argv | expect | pre-fix | post-fix | notes |" { header = 1; next }
|
|
451
|
+
/^\|[-|[:space:]]+\|[[:space:]]*$/ { next }
|
|
452
|
+
/^\|[[:space:]]*AC[0-9]+[[:space:]]*\|/ {
|
|
453
|
+
# The leading and trailing delimiters make a valid row exactly ten
|
|
454
|
+
# fields. Do not inspect or interpolate any cell until this is true.
|
|
455
|
+
if (NF != 10) { bad_shape = 1; next }
|
|
456
|
+
for (i = 2; i <= 9; i++) {
|
|
457
|
+
cell = $i
|
|
458
|
+
sub(/^[ \t]+/, "", cell)
|
|
459
|
+
sub(/[ \t]+$/, "", cell)
|
|
460
|
+
if (cell ~ /[[:cntrl:]]/) { bad_control = 1 }
|
|
461
|
+
cells[i] = cell
|
|
462
|
+
}
|
|
463
|
+
print cells[2] "\t" cells[3] "\t" cells[4] "\t" cells[5] "\t" cells[6] "\t" cells[7] "\t" cells[8] "\t" cells[9]
|
|
464
|
+
count += 1
|
|
465
|
+
next
|
|
466
|
+
}
|
|
467
|
+
/^\|/ { bad_shape = 1 }
|
|
468
|
+
END {
|
|
469
|
+
if (!header || bad_shape || bad_control || count == 0) exit 1
|
|
470
|
+
}
|
|
471
|
+
' "$1"
|
|
472
|
+
}
|
|
277
473
|
is_already_fixed() { [ "$(state_value classification)" = "ALREADY_FIXED" ]; }
|
|
278
474
|
|
|
279
475
|
phase1() {
|
|
280
|
-
local file="$trace/01-issue-summary.md" value
|
|
476
|
+
local file="$trace/01-issue-summary.md" value acceptance_count classification_count
|
|
281
477
|
check_headings "$file" '## Source' '## Observed Behavior' '## Expected Behavior' '## Acceptance Criteria' '## Classification' '## Related Issues'
|
|
282
|
-
|
|
478
|
+
trace_path_safe "$file" file || return
|
|
479
|
+
acceptance_count="$(acceptance_ids | grep -c . || true)"
|
|
480
|
+
[ "${acceptance_count:-0}" -gt 0 ] && rule_ok acceptance-criteria || rule_bad acceptance-criteria "missing AC checkbox"
|
|
283
481
|
value="$(state_value classification)"
|
|
284
482
|
case "$value" in VALID|AMBIGUOUS|ALREADY_FIXED|NOT_A_BUG|FEATURE) ;; *) rule_bad classification "invalid state value"; return;; esac
|
|
285
|
-
|
|
483
|
+
classification_count="$(normalize_terminal_cr "$file" | grep -A100 '^## Classification$' | grep -c "$value" || true)"
|
|
484
|
+
if normalize_terminal_cr "$file" | grep -A100 '^## Classification$' | grep -Eq 'VALID|AMBIGUOUS|ALREADY_FIXED|NOT_A_BUG|FEATURE' && [ "${classification_count:-0}" -gt 0 ]; then rule_ok classification
|
|
286
485
|
else rule_bad classification "artifact does not match state"; fi
|
|
287
486
|
}
|
|
288
487
|
|
|
289
488
|
phase2() {
|
|
290
|
-
local file="$trace/02-reproduction.md"
|
|
489
|
+
local file="$trace/02-reproduction.md" text_block_count
|
|
291
490
|
check_headings "$file" '## Commands Tried' '## Reproduction Verdict'
|
|
292
|
-
|
|
293
|
-
|
|
491
|
+
trace_path_safe "$file" file || return
|
|
492
|
+
text_block_count="$(normalize_terminal_cr "$file" | grep -c '^```text$' || true)"
|
|
493
|
+
[ "${text_block_count:-0}" -gt 0 ] && rule_ok reproduction-text-block || rule_bad reproduction-text-block "missing"
|
|
494
|
+
normalize_terminal_cr "$file" | grep -Eq '^- Exit code: [0-9]+' && rule_ok reproduction-exit-code || rule_bad reproduction-exit-code "missing"
|
|
294
495
|
if is_already_fixed; then check_headings "$file" '## Fixing Change'; fi
|
|
295
496
|
}
|
|
296
497
|
|
|
297
498
|
phase25() {
|
|
298
|
-
local file="$trace/02-reproduction.md" header ac row found class check pre notes reason checkpoint manifest_path diff_path
|
|
499
|
+
local file="$trace/02-reproduction.md" header ac row found class check argv expect pre post notes reason checkpoint manifest_path diff_path duplicate_check rows summary_ids
|
|
299
500
|
if is_already_fixed; then rule_ok obe-subset; return; fi
|
|
501
|
+
check_headings "$file" '## Commands Tried' '## Reproduction Verdict'
|
|
502
|
+
trace_path_safe "$file" file || return
|
|
300
503
|
header='| AC | class | check | argv | expect | pre-fix | post-fix | notes |'
|
|
301
|
-
|
|
504
|
+
acceptance_table_header "$file" && rule_ok acceptance-table || { rule_bad acceptance-table "missing exact header"; return; }
|
|
505
|
+
if ! rows="$(acceptance_table_rows "$file")"; then
|
|
506
|
+
rule_bad acceptance-table "each row must have exactly 10 pipe columns and no control bytes"
|
|
507
|
+
return
|
|
508
|
+
fi
|
|
509
|
+
# Keep Phase 2.5 bound to the same semantic verifier used at Phase 4. The
|
|
510
|
+
# helper is read-only: it validates the manifest and acceptance table, and
|
|
511
|
+
# does not invoke trace-check, so calling it through bash cannot recurse or
|
|
512
|
+
# introduce checkpoint side effects.
|
|
513
|
+
if bash "$script_dir/repro-check.sh" verify-semantics --slug "$slug" --trace-dir "$trace" >/dev/null 2>&1; then
|
|
514
|
+
rule_ok acceptance-manifest-semantics
|
|
515
|
+
else
|
|
516
|
+
rule_bad acceptance-manifest-semantics "acceptance table semantics do not match checkpoint manifest"
|
|
517
|
+
fi
|
|
302
518
|
while IFS= read -r ac; do
|
|
303
|
-
found=
|
|
519
|
+
found=0
|
|
520
|
+
while IFS=$'\t' read -r row_ac _ _ _ _ _ _ _; do
|
|
521
|
+
[ "$row_ac" = "$ac" ] && found=$((found + 1))
|
|
522
|
+
done <<EOF
|
|
523
|
+
$rows
|
|
524
|
+
EOF
|
|
304
525
|
[ "$found" -eq 1 ] && rule_ok "acceptance-$ac" || rule_bad "acceptance-$ac" "must appear exactly once"
|
|
305
526
|
done < <(acceptance_ids)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
fi
|
|
315
|
-
IFS='|' read -r _ ac class check argv expect pre post notes _ <<EOF
|
|
316
|
-
$row
|
|
527
|
+
summary_ids="$(acceptance_ids)"
|
|
528
|
+
while IFS=$'\t' read -r row_ac _ _ _ _ _ _ _; do
|
|
529
|
+
[ -n "$row_ac" ] || continue
|
|
530
|
+
found=0
|
|
531
|
+
while IFS= read -r ac; do
|
|
532
|
+
[ "$row_ac" = "$ac" ] && found=1
|
|
533
|
+
done <<EOF
|
|
534
|
+
$summary_ids
|
|
317
535
|
EOF
|
|
318
|
-
|
|
536
|
+
[ "$found" -eq 1 ] || rule_bad "acceptance-$row_ac" "table row is absent from issue summary"
|
|
537
|
+
done <<EOF
|
|
538
|
+
$rows
|
|
539
|
+
EOF
|
|
540
|
+
duplicate_check="$(printf '%s\n' "$rows" | awk -F '\t' '{ if (++seen[$3] > 1 && duplicate == "") duplicate = $3 } END { if (duplicate != "") print duplicate }')"
|
|
541
|
+
check="$duplicate_check"
|
|
542
|
+
[ -z "$check" ] && rule_ok acceptance-check-ids || rule_bad acceptance-check-ids "duplicate check id $check"
|
|
543
|
+
while IFS=$'\t' read -r ac class check argv expect pre post notes; do
|
|
544
|
+
[ -n "$ac" ] || continue
|
|
319
545
|
case "$class" in
|
|
320
|
-
DISCRIMINATING) [ "$pre" = RED ] || rule_bad "pre-fix-$ac" "DISCRIMINATING must be RED";
|
|
321
|
-
PRESERVING) [ "$pre" = GREEN ] || rule_bad "pre-fix-$ac" "PRESERVING must be GREEN";
|
|
322
|
-
NEW-SURFACE) [ "$pre" = ERROR ] || rule_bad "pre-fix-$ac" "NEW-SURFACE must be ERROR";
|
|
546
|
+
DISCRIMINATING) [ "$pre" = RED ] || rule_bad "pre-fix-$ac" "DISCRIMINATING must be RED"; trace_path_safe "$trace/repro/$check.base.log" file || rule_bad "base-log-$check" "missing or unsafe" ;;
|
|
547
|
+
PRESERVING) [ "$pre" = GREEN ] || rule_bad "pre-fix-$ac" "PRESERVING must be GREEN"; trace_path_safe "$trace/repro/$check.base.log" file || rule_bad "base-log-$check" "missing or unsafe" ;;
|
|
548
|
+
NEW-SURFACE) [ "$pre" = ERROR ] || rule_bad "pre-fix-$ac" "NEW-SURFACE must be ERROR"; trace_path_safe "$trace/repro/$check.base.log" file || rule_bad "base-log-$check" "missing or unsafe" ;;
|
|
323
549
|
NON-EXECUTABLE) case "$check" in DOCS_ONLY|HOST_ONLY|PRODUCT_DECISION|EXTERNAL_SERVICE_UNAVAILABLE) ;; *) rule_bad "non-executable-$ac" "unknown reason";; esac; [ -n "$notes" ] && [ "$notes" != '-' ] || rule_bad "notes-$ac" "required" ;;
|
|
324
550
|
*) rule_bad "class-$ac" "invalid" ;;
|
|
325
551
|
esac
|
|
326
|
-
done
|
|
552
|
+
done <<EOF
|
|
553
|
+
$rows
|
|
554
|
+
EOF
|
|
327
555
|
check_headings "$file" '## Red checkpoint'
|
|
328
|
-
checkpoint="$(
|
|
556
|
+
checkpoint="$(awk '/^checkpoint-tree-id: / { sub(/\r$/, "", $0); sub(/^checkpoint-tree-id: /, ""); print; exit }' "$file" 2>/dev/null)"
|
|
329
557
|
is_hex "$checkpoint" && [ "$checkpoint" = "$(state_value checkpoint-tree-id)" ] && rule_ok red-checkpoint || rule_bad red-checkpoint "state binding missing or invalid"
|
|
330
558
|
manifest_path="$trace/repro/checkpoint.manifest"
|
|
559
|
+
trace_path_safe "$trace/repro" dir || { rule_bad checkpoint-manifest "missing or unsafe repro directory"; return; }
|
|
331
560
|
# Header shape only - repro-check.sh owns full validation (row count, seq
|
|
332
561
|
# run, field count). The `rows=<N>` suffix is required, matching the fact
|
|
333
562
|
# that repro-check refuses a header with no count; awk rather than a
|
|
334
563
|
# `head | grep -q` pipeline so no SIGPIPE can decide the verdict, and the
|
|
335
564
|
# END guard makes an empty manifest fail instead of vacuously passing.
|
|
336
|
-
|
|
565
|
+
trace_path_safe "$manifest_path" file && awk 'NR == 1 { sub(/\r$/, "", $0); if ($0 ~ /^# issue-tracer checkpoint manifest v1 rows=[0-9]+$/) exit 0; exit 1 } END { if (NR == 0) exit 1 }' "$manifest_path" && rule_ok checkpoint-manifest || { rule_bad checkpoint-manifest "missing or invalid"; return; }
|
|
566
|
+
# A checkpoint tree has one effective blob per path. Multiple acceptance
|
|
567
|
+
# checks may share a path when they captured identical bytes; those rows can
|
|
568
|
+
# be deduplicated while deriving the tree. Divergent effective blobs for one
|
|
569
|
+
# path are unsafe, however, and must fail before the tree/path comparison can
|
|
570
|
+
# accidentally treat the manifest as a path-only set.
|
|
571
|
+
if awk -F '\t' '
|
|
572
|
+
NR > 1 {
|
|
573
|
+
if ($3 ~ /[[:cntrl:]]/) { bad = 1; next }
|
|
574
|
+
pair = length($3) ":" $3 ":" $6
|
|
575
|
+
latest_path[pair] = $3
|
|
576
|
+
latest_blob[pair] = $4
|
|
577
|
+
}
|
|
578
|
+
END {
|
|
579
|
+
for (pair in latest_path) {
|
|
580
|
+
path = latest_path[pair]
|
|
581
|
+
if (seen[path] && blob[path] != latest_blob[pair]) {
|
|
582
|
+
print "conflicting effective blobs for " path > "/dev/stderr"
|
|
583
|
+
bad = 1
|
|
584
|
+
}
|
|
585
|
+
seen[path] = 1
|
|
586
|
+
blob[path] = latest_blob[pair]
|
|
587
|
+
}
|
|
588
|
+
exit bad
|
|
589
|
+
}
|
|
590
|
+
' "$manifest_path"; then
|
|
591
|
+
rule_ok manifest-effective-blobs
|
|
592
|
+
else
|
|
593
|
+
rule_bad manifest-effective-blobs "same path has divergent effective blobs"
|
|
594
|
+
return
|
|
595
|
+
fi
|
|
337
596
|
diff_path="$(git diff-tree -r --name-only "$(state_value phase0-tree-id)" "$checkpoint" 2>/dev/null || true)"
|
|
338
597
|
while IFS= read -r check; do
|
|
339
598
|
[ -z "$check" ] && continue
|
|
@@ -347,9 +606,11 @@ phase3() {
|
|
|
347
606
|
local head tid
|
|
348
607
|
check_headings "$trace/05-fix-plan.md" '## Selected Fix' '## Candidate Fixes' '## Impact Analysis' '## Anticipated Defect-Class Sweep (Phase 4.2)'
|
|
349
608
|
check_headings "$trace/06-critic-review.md" '## Reviewed SHA / diff hash' '## Verdict' '## Check replay'
|
|
350
|
-
|
|
609
|
+
trace_path_safe "$trace/05-fix-plan.md" file || return
|
|
610
|
+
trace_path_safe "$trace/06-critic-review.md" file || return
|
|
611
|
+
awk '{ sub(/\r$/, "", $0); if ($0 ~ /^## Round [0-9]+$/) found=1 } END { exit !found }' "$trace/06-critic-review.md" 2>/dev/null && rule_ok heading-round || rule_bad heading-round "missing ## Round N heading in $(basename "$trace/06-critic-review.md")"
|
|
351
612
|
artifact_verdict_approved "$trace/06-critic-review.md" && rule_ok critic-verdict || rule_bad critic-verdict "06-critic-review.md Verdict section must be exactly APPROVE"
|
|
352
|
-
|
|
613
|
+
trace_path_safe "$trace/07-approved-plan.md" file && rule_ok approved-plan || rule_bad approved-plan "missing or unsafe"
|
|
353
614
|
# The checkpoint tree may be dirty relative to HEAD at Phase 3 (the fix is
|
|
354
615
|
# not implemented yet), so the plan-critic gate row is bound to the current
|
|
355
616
|
# HEAD commit and the current working-tree identity (tree_id), not the
|
|
@@ -359,54 +620,188 @@ phase3() {
|
|
|
359
620
|
artifact_identity_matches_gate "$trace/06-critic-review.md" plan-critic "$head" "$tid"
|
|
360
621
|
}
|
|
361
622
|
|
|
362
|
-
executable_ids() {
|
|
623
|
+
executable_ids() {
|
|
624
|
+
local file="$trace/02-reproduction.md"
|
|
625
|
+
normalize_terminal_cr "$file" | grep -E '^\|[[:space:]]*AC[0-9]+[[:space:]]*\|' | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); gsub(/^[ \t]+|[ \t]+$/, "", $4); if ($3 != "NON-EXECUTABLE") print $4}'
|
|
626
|
+
}
|
|
627
|
+
manifest_has_check_id() {
|
|
628
|
+
local manifest="$1" want="$2"
|
|
629
|
+
trace_path_safe "$manifest" file || return 1
|
|
630
|
+
awk -F '\t' -v want="$want" '
|
|
631
|
+
NR > 1 { latest[length($3) ":" $3 ":" $6] = $6 }
|
|
632
|
+
END { for (pair in latest) if (latest[pair] == want) found = 1; exit !found }
|
|
633
|
+
' "$manifest"
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
# Validate one recorded replay block against the acceptance table and the
|
|
637
|
+
# checkpoint manifest. Presence of `### Check C1` alone is not evidence: the
|
|
638
|
+
# block must carry the base/head SHA, exit status, result, verdict, and the two
|
|
639
|
+
# replay logs produced by `repro-check.sh run`. The result pair is checked
|
|
640
|
+
# against the row's class and the post-fix column, so a fabricated narrative
|
|
641
|
+
# cannot turn Phase 4 green without real replay artifacts.
|
|
642
|
+
phase4_check_evidence() {
|
|
643
|
+
local file="$1" id="$2" rows row class expect post block base_line head_line
|
|
644
|
+
local base_sha head_sha manifest_base base_exit head_exit base_result head_result verdict
|
|
645
|
+
rows="$(acceptance_table_rows "$trace/02-reproduction.md")" || {
|
|
646
|
+
rule_bad acceptance-table "missing or malformed acceptance table"
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
row="$(printf '%s\n' "$rows" | awk -F '\t' -v want="$id" '$3 == want { print; exit }')"
|
|
650
|
+
[ -n "$row" ] || { rule_bad "check-evidence-$id" "missing acceptance row"; return; }
|
|
651
|
+
IFS=$'\t' read -r _ class _ _ expect _ post _ <<EOF
|
|
652
|
+
$row
|
|
653
|
+
EOF
|
|
654
|
+
[ "$post" = GREEN ] || rule_bad "post-fix-$id" "acceptance row must record post-fix GREEN"
|
|
655
|
+
|
|
656
|
+
block="$(normalize_terminal_cr "$file" | awk -v marker="### Check $id" '
|
|
657
|
+
($0 == marker || index($0, marker " (") == 1) { found += 1; in_block = 1; next }
|
|
658
|
+
in_block && /^### Check / { exit }
|
|
659
|
+
in_block { print }
|
|
660
|
+
END { if (found != 1) exit 1 }
|
|
661
|
+
')" || {
|
|
662
|
+
rule_bad "check-evidence-$id" "must contain exactly one complete replay block"
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
base_line="$(printf '%s\n' "$block" | awk '/^- base: / { print; exit }')"
|
|
666
|
+
head_line="$(printf '%s\n' "$block" | awk '/^- head: / { print; exit }')"
|
|
667
|
+
verdict="$(printf '%s\n' "$block" | awk '/^- verdict: / { sub(/^- verdict: /, ""); print; exit }')"
|
|
668
|
+
if ! printf '%s\n' "$base_line" | grep -Eq "^- base: [0-9a-f]{40} exit=[0-9]+ result=(RED|GREEN|ERROR|FAIL|TIMEOUT|VACUOUS) log=repro/${id}\.base\.log$"; then
|
|
669
|
+
rule_bad "base-evidence-$id" "missing bound base SHA, exit, result, or replay log"
|
|
670
|
+
return
|
|
671
|
+
fi
|
|
672
|
+
if ! printf '%s\n' "$head_line" | grep -Eq "^- head: [0-9a-f]{40} exit=[0-9]+ result=(RED|GREEN|ERROR|FAIL|TIMEOUT|VACUOUS) log=repro/${id}\.head\.log$"; then
|
|
673
|
+
rule_bad "head-evidence-$id" "missing bound head SHA, exit, result, or replay log"
|
|
674
|
+
return
|
|
675
|
+
fi
|
|
676
|
+
[ "$verdict" = PASS ] || { rule_bad "verdict-$id" "replay verdict must be PASS"; return; }
|
|
677
|
+
|
|
678
|
+
base_sha="$(printf '%s\n' "$base_line" | sed -E 's/^- base: ([0-9a-f]{40}).*/\1/')"
|
|
679
|
+
head_sha="$(printf '%s\n' "$head_line" | sed -E 's/^- head: ([0-9a-f]{40}).*/\1/')"
|
|
680
|
+
base_exit="$(printf '%s\n' "$base_line" | sed -E 's/.* exit=([0-9]+) result=.*/\1/')"
|
|
681
|
+
head_exit="$(printf '%s\n' "$head_line" | sed -E 's/.* exit=([0-9]+) result=.*/\1/')"
|
|
682
|
+
base_result="$(printf '%s\n' "$base_line" | sed -E 's/.* result=([^ ]+) log=.*/\1/')"
|
|
683
|
+
head_result="$(printf '%s\n' "$head_line" | sed -E 's/.* result=([^ ]+) log=.*/\1/')"
|
|
684
|
+
# A check can cover multiple paths, and an AMEND row supersedes the earlier
|
|
685
|
+
# row for its exact (path, check-id) pair. Resolve the effective pair rows
|
|
686
|
+
# first, then select the highest sequence so an amended checkpoint's base
|
|
687
|
+
# SHA—not the historical first row—binds the replay evidence.
|
|
688
|
+
manifest_base="$(awk -F '\t' -v want="$id" '
|
|
689
|
+
NR > 1 && $6 == want {
|
|
690
|
+
pair = length($3) ":" $3 ":" $6
|
|
691
|
+
if (!(pair in latest_seq) || ($1 + 0) > latest_seq[pair]) {
|
|
692
|
+
latest_seq[pair] = $1 + 0
|
|
693
|
+
latest_base[pair] = $9
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
END {
|
|
697
|
+
for (pair in latest_seq) {
|
|
698
|
+
if (!found || latest_seq[pair] > max_seq) {
|
|
699
|
+
found = 1
|
|
700
|
+
max_seq = latest_seq[pair]
|
|
701
|
+
selected = latest_base[pair]
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (found) print selected
|
|
705
|
+
}
|
|
706
|
+
' "$manifest")"
|
|
707
|
+
[ "$base_sha" = "$manifest_base" ] || rule_bad "base-identity-$id" "base SHA does not match checkpoint manifest"
|
|
708
|
+
[ "$head_sha" = "$(git rev-parse HEAD)" ] || rule_bad "head-identity-$id" "head SHA does not match current HEAD"
|
|
709
|
+
case "$class" in
|
|
710
|
+
DISCRIMINATING) expected_base=RED; expected_head=GREEN; ;;
|
|
711
|
+
PRESERVING) expected_base=GREEN; expected_head=GREEN; ;;
|
|
712
|
+
NEW-SURFACE) expected_base=ERROR; expected_head=GREEN; ;;
|
|
713
|
+
*) rule_bad "class-$id" "invalid executable class"; return ;;
|
|
714
|
+
esac
|
|
715
|
+
[ "$base_result" = "$expected_base" ] || rule_bad "base-result-$id" "expected $expected_base, recorded $base_result"
|
|
716
|
+
[ "$head_result" = "$expected_head" ] || rule_bad "head-result-$id" "expected $expected_head, recorded $head_result"
|
|
717
|
+
[ "$head_exit" -eq 0 ] || rule_bad "head-exit-$id" "post-fix replay must exit 0"
|
|
718
|
+
case "$class" in
|
|
719
|
+
DISCRIMINATING|NEW-SURFACE)
|
|
720
|
+
[ "$base_exit" -ne 0 ] || rule_bad "base-exit-$id" "pre-fix replay must be nonzero"
|
|
721
|
+
if ! trace_path_safe "$trace/repro/$id.base.log" file || ! grep -Eq -- "$expect" "$trace/repro/$id.base.log"; then
|
|
722
|
+
rule_bad "base-log-$id" "pre-fix replay log is missing or does not match expect"
|
|
723
|
+
fi
|
|
724
|
+
;;
|
|
725
|
+
PRESERVING) [ "$base_exit" -eq 0 ] || rule_bad "base-exit-$id" "preserving replay must be green at base" ;;
|
|
726
|
+
esac
|
|
727
|
+
trace_path_safe "$trace/repro/$id.base.log" file || rule_bad "base-log-$id" "missing or unsafe replay log"
|
|
728
|
+
trace_path_safe "$trace/repro/$id.head.log" file || rule_bad "head-log-$id" "missing or unsafe replay log"
|
|
729
|
+
rule_ok "check-evidence-$id"
|
|
730
|
+
}
|
|
363
731
|
phase4() {
|
|
364
|
-
local file="$trace/08-test-results.md" id
|
|
732
|
+
local file="$trace/08-test-results.md" id manifest check_block_count deferred_clean_count rows row class
|
|
365
733
|
check_headings "$file" '## Regression Test' '## Acceptance check results' '## Quality Checks' '## Deferred-Work Scan' '## Verification Reasoning' '## Checkpoint verification'
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
734
|
+
manifest="$trace/repro/checkpoint.manifest"
|
|
735
|
+
trace_path_safe "$trace/02-reproduction.md" file || { rule_bad acceptance-source "missing or unsafe reproduction artifact"; return; }
|
|
736
|
+
trace_path_safe "$trace/repro" dir || { rule_bad recurrence-manifest "missing or unsafe repro directory"; return; }
|
|
737
|
+
trace_path_safe "$manifest" file || { rule_bad recurrence-manifest "missing or unsafe checkpoint manifest"; return; }
|
|
738
|
+
rows="$(acceptance_table_rows "$trace/02-reproduction.md")" || { rule_bad acceptance-table "missing or malformed acceptance table"; return; }
|
|
739
|
+
while IFS= read -r id; do
|
|
740
|
+
[ -z "$id" ] || {
|
|
741
|
+
check_block_count="$(normalize_terminal_cr "$file" | awk -v marker="### Check $id" '$0 == marker || index($0, marker " (") == 1 { count++ } END { print count + 0 }')"
|
|
742
|
+
[ "${check_block_count:-0}" -eq 1 ] && rule_ok "check-block-$id" || rule_bad "check-block-$id" "must appear exactly once"
|
|
743
|
+
[ "${check_block_count:-0}" -eq 1 ] && phase4_check_evidence "$file" "$id"
|
|
744
|
+
}
|
|
745
|
+
done < <(executable_ids)
|
|
746
|
+
while IFS= read -r id; do
|
|
747
|
+
[ -z "$id" ] || { manifest_has_check_id "$manifest" "$id" && rule_ok "manifest-check-$id" || rule_bad "manifest-check-$id" "executable acceptance check is missing from the effective manifest"; }
|
|
748
|
+
done < <(executable_ids)
|
|
749
|
+
if bash "$script_dir/repro-check.sh" verify-semantics --slug "$slug" --trace-dir "$trace" >/dev/null 2>&1; then
|
|
750
|
+
rule_ok acceptance-manifest-semantics
|
|
751
|
+
else
|
|
752
|
+
rule_bad acceptance-manifest-semantics "acceptance table semantics do not match checkpoint manifest"
|
|
753
|
+
fi
|
|
754
|
+
if bash "$script_dir/repro-check.sh" verify-checkpoint --slug "$slug" --trace-dir "$trace" >/dev/null 2>&1; then rule_ok checkpoint-verification; else rule_bad checkpoint-verification "verify-checkpoint failed"; fi
|
|
755
|
+
deferred_clean_count="$(normalize_terminal_cr "$file" | grep -A100 '^## Deferred-Work Scan$' | grep -c '^scan-deferred: clean' || true)"
|
|
756
|
+
[ "${deferred_clean_count:-0}" -gt 0 ] && rule_ok deferred-work-scan || rule_bad deferred-work-scan "clean result missing"
|
|
369
757
|
}
|
|
370
758
|
|
|
371
759
|
phase42() {
|
|
372
760
|
local file="$trace/08a-recurrence-sweep.md" hits rows
|
|
373
|
-
|
|
374
|
-
if grep -Eq '^no-defect-class: true$'
|
|
761
|
+
trace_path_safe "$file" file || { rule_bad recurrence-sweep "missing"; return; }
|
|
762
|
+
if normalize_terminal_cr "$file" | grep -Eq '^no-defect-class: true$'; then
|
|
375
763
|
if recurrence_justification_ok "$file"; then rule_ok recurrence-sweep; else rule_bad recurrence-sweep "fast-path Justification must have non-placeholder text"; fi
|
|
376
764
|
return
|
|
377
765
|
fi
|
|
378
766
|
check_headings "$file" '## Defect Class' '## Predicates and Results' '## Dispositions' '## Guardrail'
|
|
379
|
-
hits="$(grep -E '^- Predicate.*hits: [0-9]+'
|
|
380
|
-
grep -Eq '^- Predicate.*hits: [0-9]+'
|
|
381
|
-
rows="$(awk '/^## Dispositions$/{in_table=1; next} /^## /{in_table=0} in_table && /^\|/ && $0 !~ /^\|[ -]*\|/ {n++} END {print n-1}'
|
|
767
|
+
hits="$(normalize_terminal_cr "$file" | grep -E '^- Predicate.*hits: [0-9]+' | sed -E 's/.*hits: ([0-9]+).*/\1/' | awk '{s += $1} END {print s + 0}')"
|
|
768
|
+
normalize_terminal_cr "$file" | grep -Eq '^- Predicate.*hits: [0-9]+' && rule_ok recurrence-predicates || rule_bad recurrence-predicates "missing hit counts"
|
|
769
|
+
rows="$(normalize_terminal_cr "$file" | awk '/^## Dispositions$/{in_table=1; next} /^## /{in_table=0} in_table && /^\|/ && $0 !~ /^\|[ -]*\|/ {n++} END {print n-1}')"
|
|
382
770
|
[ "$hits" -eq "$rows" ] && rule_ok recurrence-dispositions || rule_bad recurrence-dispositions "rows ($rows) do not equal hits ($hits)"
|
|
383
|
-
grep -A100 '^## Guardrail$'
|
|
771
|
+
normalize_terminal_cr "$file" | grep -A100 '^## Guardrail$' | tr '\n' ' ' | grep -Eq '### Check .*RED.*GREEN' && rule_ok recurrence-guardrail || rule_bad recurrence-guardrail "missing RED then GREEN check"
|
|
384
772
|
}
|
|
385
773
|
|
|
386
774
|
phase45() {
|
|
387
775
|
clean_tree
|
|
388
776
|
local file="$trace/08b-implementation-review.md" head tid
|
|
389
777
|
check_headings "$file" '## Reviewed SHA / diff hash' '## Verdict' '## Independently re-run' '## Check integrity' '## Deferred / Scoped-Out / Unwired'
|
|
778
|
+
trace_path_safe "$file" file || return
|
|
390
779
|
artifact_verdict_approved "$file" && rule_ok artifact-verdict-implementation-review || rule_bad artifact-verdict-implementation-review "must contain APPROVE under ## Verdict"
|
|
391
780
|
head="$(git rev-parse HEAD)"; tid="$(tree_id)"
|
|
392
781
|
state_gate implementation-review APPROVE "$head" "$tid"
|
|
393
782
|
artifact_identity_matches_gate "$file" implementation-review "$head" "$tid"
|
|
394
783
|
}
|
|
395
784
|
phase46() {
|
|
396
|
-
local ac file="$trace/09-final-critic.md" head tid
|
|
785
|
+
local ac file="$trace/09-final-critic.md" head tid evidence_count
|
|
397
786
|
clean_tree
|
|
398
787
|
check_headings "$file" '## Reviewed SHA / diff hash' '## Verdict' '## Review Freshness' '## Deferred / Scoped-Out / Unwired' '## Acceptance criteria evidence'
|
|
788
|
+
trace_path_safe "$file" file || return
|
|
789
|
+
trace_path_safe "$trace/01-issue-summary.md" file || { rule_bad acceptance-source "missing or unsafe issue summary"; return; }
|
|
399
790
|
artifact_verdict_approved "$file" && rule_ok artifact-verdict-final-critic || rule_bad artifact-verdict-final-critic "must contain APPROVE under ## Verdict"
|
|
400
791
|
head="$(git rev-parse HEAD)"; tid="$(tree_id)"
|
|
401
792
|
state_gate final-critic APPROVE "$head" "$tid"
|
|
402
793
|
artifact_identity_matches_gate "$file" final-critic "$head" "$tid"
|
|
403
|
-
while IFS= read -r ac; do
|
|
794
|
+
while IFS= read -r ac; do
|
|
795
|
+
evidence_count="$(normalize_terminal_cr "$file" | grep -A100 '^## Acceptance criteria evidence$' | grep -c "$ac" || true)"
|
|
796
|
+
[ "${evidence_count:-0}" -gt 0 ] && rule_ok "final-$ac" || rule_bad "final-$ac" "missing evidence"
|
|
797
|
+
done < <(acceptance_ids)
|
|
404
798
|
}
|
|
405
799
|
phase5() {
|
|
406
800
|
if is_already_fixed; then rule_ok obe-subset; return; fi
|
|
407
801
|
local file="$trace/10-pr-body.md" merge_value pr_head_line pr_head_sha
|
|
408
802
|
check_headings "$file" '## Acceptance Criteria -> Evidence' '## Waivers (or none)'
|
|
409
|
-
|
|
803
|
+
trace_path_safe "$file" file || return
|
|
804
|
+
pr_head_line="$(awk '{ sub(/\r$/, "", $0); if ($0 ~ /^PR head: [0-9a-f]{40}$/) { print; exit } }' "$file" 2>/dev/null || true)"
|
|
410
805
|
if [ -z "$pr_head_line" ]; then
|
|
411
806
|
rule_bad pr-head "missing PR head: <40-hex> line"
|
|
412
807
|
else
|
|
@@ -423,8 +818,9 @@ phase5() {
|
|
|
423
818
|
merge_check() {
|
|
424
819
|
local file="$trace/10b-merge-approval.md" pr final
|
|
425
820
|
check_headings "$file" '## User approval (verbatim)' '## PR head SHA' '## Final critic reviewed-commit'
|
|
426
|
-
|
|
427
|
-
|
|
821
|
+
trace_path_safe "$file" file || return
|
|
822
|
+
pr="$(normalize_terminal_cr "$file" | grep -A3 '^## PR head SHA$' | grep -Eo '[0-9a-f]{40}' | head -n1 || true)"
|
|
823
|
+
final="$(normalize_terminal_cr "$file" | grep -A3 '^## Final critic reviewed-commit$' | grep -Eo '[0-9a-f]{40}' | head -n1 || true)"
|
|
428
824
|
is_hex "$pr" && [ "$pr" = "$final" ] && rule_ok merge-sha-binding || rule_bad merge-sha-binding "PR and final critic SHA differ"
|
|
429
825
|
state_gate merge-approval RECORDED "" ""
|
|
430
826
|
echo 'NOTE: human-enforced gate; this validator checks presence and binding only'
|
|
@@ -444,17 +840,47 @@ while [ "$#" -gt 0 ]; do
|
|
|
444
840
|
done
|
|
445
841
|
valid_slug "$slug" || { echo "trace-check: invalid slug" >&2; exit 2; }
|
|
446
842
|
[ -n "$trace" ] || trace="$root/.agents/issue-traces/$slug"
|
|
843
|
+
trace="$(to_shell_path "$trace")"
|
|
844
|
+
validate_trace_dir "$trace"
|
|
845
|
+
# Use the same canonical spelling that validate_trace_dir checked for all
|
|
846
|
+
# subsequent artifact reads. Windows may accept an existing 8.3 alias for the
|
|
847
|
+
# explicit directory while pwd -P exposes the long spelling; retaining the
|
|
848
|
+
# alias here would make trace_path_safe compare unlike path strings. Preserve
|
|
849
|
+
# the established exit-1 result for a valid-but-absent default trace directory.
|
|
850
|
+
if [ -d "$trace" ]; then
|
|
851
|
+
trace="$(cd "$trace" 2>/dev/null && pwd -P)" || {
|
|
852
|
+
echo "FAIL state: could not resolve canonical trace root $trace" >&2
|
|
853
|
+
exit 2
|
|
854
|
+
}
|
|
855
|
+
fi
|
|
447
856
|
state="$trace/state.md"
|
|
448
|
-
if [ ! -d "$trace" ]
|
|
857
|
+
if [ ! -d "$trace" ]; then
|
|
858
|
+
echo "FAIL state: missing or unsafe trace directory $trace"
|
|
859
|
+
exit 1
|
|
860
|
+
fi
|
|
861
|
+
trace_root_real="$(cd "$trace" 2>/dev/null && pwd -P)" || {
|
|
862
|
+
echo "FAIL state: could not resolve canonical trace root $trace" >&2
|
|
863
|
+
exit 2
|
|
864
|
+
}
|
|
865
|
+
case "$trace_root_real/" in
|
|
866
|
+
"$root_real/.agents/issue-traces/"*) ;;
|
|
867
|
+
*) echo "FAIL state: canonical trace root escapes .agents/issue-traces" >&2; exit 2;;
|
|
868
|
+
esac
|
|
869
|
+
if ! trace_path_safe "$trace" dir; then
|
|
870
|
+
echo "FAIL state: missing or unsafe trace directory $trace"
|
|
871
|
+
exit 1
|
|
872
|
+
fi
|
|
873
|
+
if ! trace_path_safe "$state" file; then
|
|
449
874
|
echo "FAIL state: missing $state"
|
|
450
875
|
exit 1
|
|
451
876
|
fi
|
|
452
|
-
protocol_line="$(grep -c '^protocol: '
|
|
877
|
+
protocol_line="$(state_lines | grep -c '^protocol: ' || true)"
|
|
453
878
|
protocol_value="$(state_value protocol)"
|
|
454
879
|
if [ "${protocol_line:-0}" -eq 0 ]; then
|
|
455
880
|
# No protocol line at all: legacy v2 ledger unless a v3-only key is present,
|
|
456
881
|
# in which case this is a v3 ledger that had its protocol line stripped.
|
|
457
|
-
|
|
882
|
+
v3_marker_lines="$(state_lines | grep -Ec '^(phase0-tree-id|checkpoint-tree-id|handshake): ' || true)"
|
|
883
|
+
if [ "${v3_marker_lines:-0}" -gt 0 ]; then
|
|
458
884
|
echo "FAIL state-protocol: missing (v3 ledger without protocol line)"
|
|
459
885
|
exit 1
|
|
460
886
|
fi
|