opencode-swarm 7.164.13 → 7.165.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/skills/durable-session-state/SKILL.md +1 -1
- package/.opencode/skills/issue-tracer/SKILL.md +140 -286
- package/.opencode/skills/issue-tracer/assets/pr-template.md +13 -7
- package/.opencode/skills/issue-tracer/references/acceptance-checks.md +66 -0
- package/.opencode/skills/issue-tracer/references/critic-gate.md +53 -11
- package/.opencode/skills/issue-tracer/references/evidence-artifacts.md +128 -23
- package/.opencode/skills/issue-tracer/references/full-resolution-contract.md +69 -0
- package/.opencode/skills/issue-tracer/references/install.md +31 -9
- package/.opencode/skills/issue-tracer/references/localization-playbook.md +26 -14
- package/.opencode/skills/issue-tracer/references/method-provenance.md +19 -3
- package/.opencode/skills/issue-tracer/references/phase-0-setup.md +90 -0
- package/.opencode/skills/issue-tracer/references/phase-1-intake.md +43 -0
- package/.opencode/skills/issue-tracer/references/untrusted-content.md +13 -5
- package/.opencode/skills/issue-tracer/scripts/repro-check.sh +416 -0
- package/.opencode/skills/issue-tracer/scripts/trace-check.sh +479 -0
- package/.opencode/skills/issue-tracer/scripts/trace-init.sh +96 -6
- package/dist/cli/{coder-settlement-em2fgg4p.js → coder-settlement-89nt4j7g.js} +3 -3
- package/dist/cli/{config-doctor-1m4mrybp.js → config-doctor-gm6f5chm.js} +2 -2
- package/dist/cli/{core-zbzdffha.js → core-91p43zwj.js} +1 -1
- package/dist/cli/{curator-tn3x5j02.js → curator-4bs2hmy9.js} +19 -19
- package/dist/cli/{curator-llm-factory-j76c96t7.js → curator-llm-factory-fhbjwar0.js} +19 -19
- package/dist/cli/{evidence-summary-service-7cn9z4sn.js → evidence-summary-service-g1xd8egd.js} +6 -6
- package/dist/cli/{gate-evidence-deyzt3hz.js → gate-evidence-gdmgsz7c.js} +2 -2
- package/dist/cli/{guardrail-explain-4k2pqj46.js → guardrail-explain-1nfbtpgg.js} +20 -20
- package/dist/cli/{guardrail-log-cnbv6vry.js → guardrail-log-nyggdv04.js} +3 -3
- package/dist/cli/{guardrail-reset-xhh61cax.js → guardrail-reset-rp9jq0gj.js} +19 -19
- package/dist/cli/{hive-promoter-1m8zpz4d.js → hive-promoter-39xb4tx2.js} +19 -19
- package/dist/cli/{index-asey3tb3.js → index-0e9jh9c7.js} +1 -1
- package/dist/cli/{index-y5hfk7ya.js → index-1v6vc90w.js} +3 -3
- package/dist/cli/{index-7z9zqxqt.js → index-2k53pfnc.js} +1 -1
- package/dist/cli/{index-s3k7xbqd.js → index-3xyjvx24.js} +2 -2
- package/dist/cli/{index-bvkd7at6.js → index-4gmt79q9.js} +1 -1
- package/dist/cli/{index-d224mhr9.js → index-58be2gtd.js} +2 -2
- package/dist/cli/{index-mpf1tvwf.js → index-5ythtd9t.js} +3 -3
- package/dist/cli/{index-7f2r3ysr.js → index-6be5caj8.js} +3 -3
- package/dist/cli/{index-fnyw5rzg.js → index-7h94rfem.js} +3 -3
- package/dist/cli/{index-hhzzyjm3.js → index-afpkqsdb.js} +1 -1
- package/dist/cli/{index-grn6tkbt.js → index-c4ddtrqg.js} +4 -4
- package/dist/cli/{index-74514zkh.js → index-e6n7cemz.js} +1 -1
- package/dist/cli/{index-g9weyhq6.js → index-g8wdekzz.js} +1 -1
- package/dist/cli/{index-qtxevgdx.js → index-gn6ybsc4.js} +3 -3
- package/dist/cli/{index-4fhcbe14.js → index-hdakb4n9.js} +1 -1
- package/dist/cli/{index-c98rjgjq.js → index-hwz46jj7.js} +50 -50
- package/dist/cli/{index-zb6gdym8.js → index-k1ntjzmf.js} +1 -1
- package/dist/cli/{index-g11h1qpk.js → index-n3pjcks8.js} +5 -5
- package/dist/cli/{index-5h60rtaa.js → index-p7hdp3rp.js} +1 -1
- package/dist/cli/{index-3c0z1jnc.js → index-qzvry1n1.js} +21 -21
- package/dist/cli/{index-f8jdar6q.js → index-r5hj3nfe.js} +2 -2
- package/dist/cli/{index-ndjjvxzz.js → index-y1zhpfk8.js} +1 -1
- package/dist/cli/{index-0mwjamha.js → index-zycp24b9.js} +1 -1
- package/dist/cli/index.js +19 -19
- package/dist/cli/{knowledge-escalator-3fax9y3s.js → knowledge-escalator-eq07pcj3.js} +6 -6
- package/dist/cli/{knowledge-events-9cpdyhmb.js → knowledge-events-rv9q9mkm.js} +4 -4
- package/dist/cli/{knowledge-store-y35rka0p.js → knowledge-store-cxcr0ttk.js} +1 -1
- package/dist/cli/{knowledge-validator-fd761gpx.js → knowledge-validator-8qtd9ys0.js} +2 -2
- package/dist/cli/{pending-delegations-t3y19j3n.js → pending-delegations-dp3g8e7k.js} +2 -2
- package/dist/cli/{pr-subscriptions-00zp69mt.js → pr-subscriptions-3vx06rar.js} +1 -1
- package/dist/cli/{pr-workflow-gate-2h0j8c92.js → pr-workflow-gate-bbet0apk.js} +19 -19
- package/dist/cli/{scan-cursor-wppe2gxw.js → scan-cursor-zwm0xng7.js} +2 -2
- package/dist/cli/{schema-sec49ppy.js → schema-ebfxgya0.js} +1 -1
- package/dist/cli/{scope-persistence-59eyqaz4.js → scope-persistence-6c55akfz.js} +5 -5
- package/dist/cli/{skill-generator-56rbmpj7.js → skill-generator-mv5jxzsh.js} +7 -7
- package/dist/cli/{snapshot-coordination-init-dnkdc7gx.js → snapshot-coordination-init-n556wa18.js} +19 -19
- package/dist/cli/{worktree-collision-ownership-fj9n4mhk.js → worktree-collision-ownership-pp4z4z8p.js} +2 -2
- package/dist/cli/{worktree-isolation-vd1s86ya.js → worktree-isolation-69hz9dgg.js} +19 -19
- package/dist/config/skill-mirrors.d.ts +3 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# trace-check.sh - read-only validation for issue-tracer v3 evidence.
|
|
3
|
+
set -eu
|
|
4
|
+
export LC_ALL=C
|
|
5
|
+
|
|
6
|
+
to_shell_path() {
|
|
7
|
+
case "$(uname -s 2>/dev/null || true)" in
|
|
8
|
+
MINGW* | MSYS* | CYGWIN*)
|
|
9
|
+
if command -v cygpath >/dev/null 2>&1; then cygpath -u "$1"; return; fi
|
|
10
|
+
;;
|
|
11
|
+
esac
|
|
12
|
+
printf '%s\n' "$1"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "trace-check: not inside a git work tree" >&2; exit 2; }
|
|
16
|
+
root="$(to_shell_path "$root")"
|
|
17
|
+
script_dir="$(cd "$(dirname "$0")" && pwd -P)"
|
|
18
|
+
failed=0
|
|
19
|
+
legacy=0
|
|
20
|
+
|
|
21
|
+
usage() { echo "usage: trace-check.sh {tree-id|handshake|phase <phase> --slug <slug> [--trace-dir <dir>]|merge --slug <slug>}" >&2; exit 2; }
|
|
22
|
+
valid_slug() { case "$1" in ''|*[!a-z0-9-]*) return 1;; *) return 0;; esac; }
|
|
23
|
+
trim() { printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; }
|
|
24
|
+
|
|
25
|
+
tree_id() {
|
|
26
|
+
# Trace artifacts under .agents/issue-traces/ must never affect this
|
|
27
|
+
# identity. trace-init.sh writes that directory to info/exclude, but that
|
|
28
|
+
# entry is an unenforced convention, so after staging the working tree
|
|
29
|
+
# (tracked + untracked-not-ignored, exactly like `git add -A .`) the trace
|
|
30
|
+
# directory is removed from the temporary index explicitly. Never use
|
|
31
|
+
# `add -f` here: it would stage gitignored content (node_modules, build
|
|
32
|
+
# output), making the identity machine-specific and writing ignored blobs
|
|
33
|
+
# into the real object store.
|
|
34
|
+
local index
|
|
35
|
+
index="$(mktemp "${TMPDIR:-/tmp}/issue-tracer-index.XXXXXX")"
|
|
36
|
+
rm -f "$index"
|
|
37
|
+
if ! GIT_INDEX_FILE="$index" git -C "$root" read-tree HEAD \
|
|
38
|
+
|| ! GIT_INDEX_FILE="$index" git -C "$root" add -A -- . \
|
|
39
|
+
|| ! GIT_INDEX_FILE="$index" git -C "$root" rm -r --cached --ignore-unmatch -q -- .agents/issue-traces \
|
|
40
|
+
|| ! GIT_INDEX_FILE="$index" git -C "$root" write-tree; then
|
|
41
|
+
rm -f "$index"
|
|
42
|
+
return 1
|
|
43
|
+
fi
|
|
44
|
+
rm -f "$index"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handshake() {
|
|
48
|
+
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)"
|
|
50
|
+
[ -n "$version" ] || version="unknown"
|
|
51
|
+
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
|
+
if [ ! -f "$candidate" ]; then
|
|
53
|
+
verdict="ABSENT"
|
|
54
|
+
else
|
|
55
|
+
value="$(grep -m1 '^ version:' "$candidate" 2>/dev/null | sed 's/^ version:[[:space:]]*//' || true)"
|
|
56
|
+
shim="$(grep -m1 '^shim:' "$candidate" 2>/dev/null | sed 's/^shim:[[:space:]]*//' || true)"
|
|
57
|
+
if [ "$value" = "$version" ] && [ "$shim" = "true" ]; then verdict="SHIM"
|
|
58
|
+
elif [ "$value" = "$version" ]; then verdict="MATCH"
|
|
59
|
+
else verdict="STALE:$candidate"; fi
|
|
60
|
+
fi
|
|
61
|
+
echo "handshake: $verdict $candidate"
|
|
62
|
+
case "$verdict" in
|
|
63
|
+
STALE:*) worst="$verdict" ;;
|
|
64
|
+
ABSENT) case "$worst" in STALE:*) ;; *) worst="ABSENT";; esac ;;
|
|
65
|
+
SHIM) if [ "$worst" = "MATCH" ]; then worst="SHIM"; fi ;;
|
|
66
|
+
esac
|
|
67
|
+
done
|
|
68
|
+
echo "handshake-summary: $worst"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
rule_ok() { echo "OK $1"; }
|
|
72
|
+
rule_bad() {
|
|
73
|
+
if [ "$legacy" -eq 1 ]; then echo "WARN $1: $2"; else echo "FAIL $1: $2"; failed=1; fi
|
|
74
|
+
}
|
|
75
|
+
state_value() { awk -F ': ' -v key="$1" '$1 == key { print substr($0, length(key) + 3); exit }' "$state" 2>/dev/null || true; }
|
|
76
|
+
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
|
+
|
|
78
|
+
check_headings() {
|
|
79
|
+
local file="$1"; shift
|
|
80
|
+
if [ ! -f "$file" ]; then rule_bad "artifact-$(basename "$file")" "missing"; return; fi
|
|
81
|
+
local heading count
|
|
82
|
+
for heading in "$@"; do
|
|
83
|
+
count="$(grep -Fx "$heading" "$file" 2>/dev/null | wc -l | tr -d ' ')"
|
|
84
|
+
if [ "$count" -eq 1 ]; then rule_ok "heading-${heading#\#\# }"
|
|
85
|
+
elif [ "$count" -gt 1 ]; then rule_bad "duplicate-heading-${heading#\#\# }" "in $(basename "$file")"
|
|
86
|
+
else rule_bad "heading-${heading#\#\# }" "missing in $(basename "$file")"; fi
|
|
87
|
+
done
|
|
88
|
+
# 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 < <(grep '^## ' "$file" 2>/dev/null | sort | uniq -d)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Parse the `## Gates` table row-by-row (split on '|', trim each cell) and
|
|
93
|
+
# return success (0) iff a row exists whose gate/verdict/commit/treeid all
|
|
94
|
+
# match. An empty commit/treeid argument means "don't filter on that column".
|
|
95
|
+
# An unanchored substring match here previously let a DISAPPROVED row satisfy
|
|
96
|
+
# an APPROVE gate, since "APPROVE|RECORDED" as a regex also matches
|
|
97
|
+
# "DISAPPROVED"; matching is done cell-by-cell after trimming instead.
|
|
98
|
+
gate_row_exists() {
|
|
99
|
+
local gate="$1" verdict_want="$2" commit="$3" treeid="$4" line g v c t
|
|
100
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
101
|
+
case "$line" in '|'*) ;; *) continue;; esac
|
|
102
|
+
IFS='|' read -r _ g v c t _ <<EOF
|
|
103
|
+
$line
|
|
104
|
+
EOF
|
|
105
|
+
g="$(trim "$g")"; v="$(trim "$v")"; c="$(trim "$c")"; t="$(trim "$t")"
|
|
106
|
+
[ "$g" = "$gate" ] || continue
|
|
107
|
+
[ "$v" = "$verdict_want" ] || continue
|
|
108
|
+
[ -z "$commit" ] || [ "$c" = "$commit" ] || continue
|
|
109
|
+
[ -z "$treeid" ] || [ "$t" = "$treeid" ] || continue
|
|
110
|
+
return 0
|
|
111
|
+
done < "$state"
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
state_gate() {
|
|
116
|
+
local gate="$1" verdict_want="$2" commit="$3" treeid="$4"
|
|
117
|
+
gate_row_exists "$gate" "$verdict_want" "$commit" "$treeid" && rule_ok "gate-$gate" || rule_bad "gate-$gate" "missing approved bound row"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Extract the section starting at a `## <heading>` line up to (but not
|
|
121
|
+
# including) the next `## ` heading or EOF, then require EXACTLY one verdict
|
|
122
|
+
# line in the section, and that line must be `APPROVE` or `Verdict: APPROVE`.
|
|
123
|
+
# Blank lines and template-guidance lines (starting with `[`) are ignored.
|
|
124
|
+
# Any additional verdict line - bare or `Verdict: X` - for NEEDS_REVISION,
|
|
125
|
+
# BLOCKED, DISAPPROVED, or REJECTED fails the section even if an APPROVE line
|
|
126
|
+
# is also present, so a stray leftover verdict cannot be shadowed by a later
|
|
127
|
+
# APPROVE.
|
|
128
|
+
artifact_verdict_approved() {
|
|
129
|
+
local file="$1"
|
|
130
|
+
[ -f "$file" ] || return 1
|
|
131
|
+
awk '
|
|
132
|
+
/^## Verdict$/ { infield = 1; next }
|
|
133
|
+
/^## / { infield = 0 }
|
|
134
|
+
infield {
|
|
135
|
+
line = $0
|
|
136
|
+
sub(/^[[:space:]]+/, "", line); sub(/[[:space:]]+$/, "", line)
|
|
137
|
+
if (line == "") next
|
|
138
|
+
if (line ~ /^\[.*\]$/) next
|
|
139
|
+
verdict = line
|
|
140
|
+
if (line ~ /^Verdict: /) { sub(/^Verdict: /, "", verdict) }
|
|
141
|
+
else if (line !~ /^[A-Z_-]+$/) { next }
|
|
142
|
+
if (verdict == "APPROVE" || verdict == "NEEDS_REVISION" || verdict == "BLOCKED" || verdict == "DISAPPROVED" || verdict == "REJECTED") {
|
|
143
|
+
count++
|
|
144
|
+
if (verdict == "APPROVE") { approve_count++ } else { other_count++ }
|
|
145
|
+
} else {
|
|
146
|
+
unknown_count++
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
END { exit !(count == 1 && approve_count == 1 && other_count == 0 && unknown_count == 0) }
|
|
150
|
+
' "$file" 2>/dev/null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Extract the `## Reviewed SHA / diff hash` section from an artifact and
|
|
154
|
+
# require exactly one line matching `reviewed-commit: <40hex>` and exactly
|
|
155
|
+
# one matching `tree-id: <40hex>`. Sets ARTIFACT_COMMIT / ARTIFACT_TREE
|
|
156
|
+
# (empty unless exactly one matching line was found) and
|
|
157
|
+
# ARTIFACT_COMMIT_COUNT / ARTIFACT_TREE_COUNT (the actual match counts, so
|
|
158
|
+
# callers can distinguish "0 matches" from "duplicate matches").
|
|
159
|
+
artifact_identity() {
|
|
160
|
+
local file="$1" section
|
|
161
|
+
section="$(awk '
|
|
162
|
+
/^## Reviewed SHA \/ diff hash$/ { infield = 1; next }
|
|
163
|
+
/^## / { infield = 0 }
|
|
164
|
+
infield { print }
|
|
165
|
+
' "$file" 2>/dev/null || true)"
|
|
166
|
+
local commit_matches tree_matches
|
|
167
|
+
# Count every prefixed line (well-formed or not) so a malformed duplicate
|
|
168
|
+
# such as "reviewed-commit: stale" cannot hide behind one valid line; the
|
|
169
|
+
# sole surviving line must then be well-formed to yield a value.
|
|
170
|
+
commit_matches="$(printf '%s\n' "$section" | grep -E '^reviewed-commit:' || true)"
|
|
171
|
+
tree_matches="$(printf '%s\n' "$section" | grep -E '^tree-id:' || true)"
|
|
172
|
+
ARTIFACT_COMMIT_COUNT="$(printf '%s\n' "$commit_matches" | grep -c . || true)"
|
|
173
|
+
ARTIFACT_TREE_COUNT="$(printf '%s\n' "$tree_matches" | grep -c . || true)"
|
|
174
|
+
ARTIFACT_COMMIT=""
|
|
175
|
+
ARTIFACT_TREE=""
|
|
176
|
+
if [ "$ARTIFACT_COMMIT_COUNT" -eq 1 ] && printf '%s\n' "$commit_matches" | grep -Eq '^reviewed-commit: [0-9a-f]{40}$'; then
|
|
177
|
+
ARTIFACT_COMMIT="$(printf '%s\n' "$commit_matches" | sed 's/^reviewed-commit: //')"
|
|
178
|
+
fi
|
|
179
|
+
if [ "$ARTIFACT_TREE_COUNT" -eq 1 ] && printf '%s\n' "$tree_matches" | grep -Eq '^tree-id: [0-9a-f]{40}$'; then
|
|
180
|
+
ARTIFACT_TREE="$(printf '%s\n' "$tree_matches" | sed 's/^tree-id: //')"
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Require the artifact's own `## Reviewed SHA / diff hash` identity to equal
|
|
185
|
+
# the expected commit/tree-id (the same values state_gate was called with for
|
|
186
|
+
# this phase), AND require a `<gate> | APPROVE | <expected-commit> |
|
|
187
|
+
# <expected-tree>` row to exist in the ledger. Checking only "the first
|
|
188
|
+
# APPROVE row for this gate" here (regardless of which commit/tree it names)
|
|
189
|
+
# previously let a STALE re-review row satisfy a CURRENT artifact and vice
|
|
190
|
+
# versa, once the append-only re-review path produced two APPROVE rows for
|
|
191
|
+
# the same gate.
|
|
192
|
+
artifact_identity_matches_gate() {
|
|
193
|
+
local file="$1" gate="$2" expected_commit="$3" expected_treeid="$4"
|
|
194
|
+
artifact_identity "$file"
|
|
195
|
+
if [ "$ARTIFACT_COMMIT_COUNT" -ne 1 ] || [ "$ARTIFACT_TREE_COUNT" -ne 1 ]; then
|
|
196
|
+
rule_bad "artifact-identity-$gate" "expected exactly one reviewed-commit and one tree-id line"
|
|
197
|
+
elif [ -n "$ARTIFACT_COMMIT" ] && [ "$ARTIFACT_COMMIT" = "$expected_commit" ] \
|
|
198
|
+
&& [ -n "$ARTIFACT_TREE" ] && [ "$ARTIFACT_TREE" = "$expected_treeid" ] \
|
|
199
|
+
&& gate_row_exists "$gate" APPROVE "$expected_commit" "$expected_treeid"; then
|
|
200
|
+
rule_ok "artifact-identity-$gate"
|
|
201
|
+
else
|
|
202
|
+
rule_bad "artifact-identity-$gate" "$(basename "$file") reviewed-commit/tree-id does not match the current $gate gate row"
|
|
203
|
+
fi
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
recurrence_justification_ok() {
|
|
207
|
+
local file="$1"
|
|
208
|
+
awk '
|
|
209
|
+
/^## Justification$/ { infield = 1; next }
|
|
210
|
+
/^## / { infield = 0 }
|
|
211
|
+
infield {
|
|
212
|
+
line = $0
|
|
213
|
+
sub(/^[[:space:]]+/, "", line); sub(/[[:space:]]+$/, "", line)
|
|
214
|
+
if (line == "") next
|
|
215
|
+
if (line ~ /^\[.*\]$/) next
|
|
216
|
+
found = 1
|
|
217
|
+
}
|
|
218
|
+
END { exit !found }
|
|
219
|
+
' "$file" 2>/dev/null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
clean_tree() {
|
|
223
|
+
local dirty
|
|
224
|
+
dirty="$(git -C "$root" status --porcelain | awk '$0 !~ /^.. \.agents\/issue-traces\//')"
|
|
225
|
+
[ -z "$dirty" ] && rule_ok clean-tree || rule_bad clean-tree "working tree has non-trace changes"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
phase0() {
|
|
229
|
+
local keys key expected actual previous=0 line fresh
|
|
230
|
+
keys='protocol phase tier classification base-ref base-sha freshness phase0-tree-id checkpoint-tree-id handshake tools merge next-action'
|
|
231
|
+
for key in $keys; do
|
|
232
|
+
line="$(grep -n "^$key: " "$state" 2>/dev/null | cut -d: -f1 | head -n1 || true)"
|
|
233
|
+
if [ -z "$line" ]; then rule_bad "state-$key" "missing"; continue; fi
|
|
234
|
+
if [ "$(grep -c "^$key: " "$state" 2>/dev/null || true)" -ne 1 ]; then rule_bad "state-$key" "must appear once"; fi
|
|
235
|
+
if [ "$line" -le "$previous" ]; then rule_bad "state-order" "$key is out of order"; fi
|
|
236
|
+
previous="$line"
|
|
237
|
+
done
|
|
238
|
+
[ "$(state_value protocol)" = "3.0.0" ] && rule_ok protocol || rule_bad protocol "expected 3.0.0"
|
|
239
|
+
is_hex "$(state_value base-sha)" && rule_ok base-sha || rule_bad base-sha "must be 40 hex"
|
|
240
|
+
is_hex "$(state_value phase0-tree-id)" && rule_ok phase0-tree-id || rule_bad phase0-tree-id "must be 40 hex"
|
|
241
|
+
# Accept only these exact forms:
|
|
242
|
+
# synced -> OK
|
|
243
|
+
# behind:<n> -> FAIL (must sync first)
|
|
244
|
+
# fetch-failed:<reason> user-override:"<non-empty>" -> OK (explicit override)
|
|
245
|
+
# fetch-failed:<reason> -> FAIL (fail closed)
|
|
246
|
+
# user-override:"<non-empty>" -> OK (standalone override)
|
|
247
|
+
# anything else -> FAIL (unknown value)
|
|
248
|
+
fresh="$(state_value freshness)"
|
|
249
|
+
case "$fresh" in
|
|
250
|
+
unset|'') rule_bad freshness "must be recorded" ;;
|
|
251
|
+
synced) rule_ok freshness ;;
|
|
252
|
+
behind:*) rule_bad freshness "behind, sync before proceeding" ;;
|
|
253
|
+
fetch-failed:*)
|
|
254
|
+
# Fully anchored: fetch-failed:<reason, no spaces/quotes> user-override:"<non-empty>"
|
|
255
|
+
# and nothing else trailing. An unanchored match previously let trailing
|
|
256
|
+
# garbage after a valid override slip through as OK.
|
|
257
|
+
if printf '%s' "$fresh" | grep -Eq '^fetch-failed:[^[:space:]"]+ user-override:"[^"]+"$'; then
|
|
258
|
+
rule_ok freshness
|
|
259
|
+
else
|
|
260
|
+
rule_bad freshness-fail-closed "fetch failure lacks user override"
|
|
261
|
+
fi
|
|
262
|
+
;;
|
|
263
|
+
user-override:*)
|
|
264
|
+
if printf '%s' "$fresh" | grep -Eq '^user-override:"[^"]+"$'; then
|
|
265
|
+
rule_ok freshness
|
|
266
|
+
else
|
|
267
|
+
rule_bad freshness "unknown value"
|
|
268
|
+
fi
|
|
269
|
+
;;
|
|
270
|
+
*) rule_bad freshness "unknown value" ;;
|
|
271
|
+
esac
|
|
272
|
+
case "$(state_value tier)" in S|M|L) rule_ok tier;; *) rule_bad tier "must be S, M, or L";; esac
|
|
273
|
+
[ "$(state_value handshake)" != "unset" ] && [ -n "$(state_value handshake)" ] && rule_ok handshake || rule_bad handshake "must be recorded"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
acceptance_ids() { grep -E '^- \[[ x]\] AC[0-9]+:' "$trace/01-issue-summary.md" 2>/dev/null | sed -E 's/^- \[[ x]\] (AC[0-9]+):.*/\1/'; }
|
|
277
|
+
is_already_fixed() { [ "$(state_value classification)" = "ALREADY_FIXED" ]; }
|
|
278
|
+
|
|
279
|
+
phase1() {
|
|
280
|
+
local file="$trace/01-issue-summary.md" value
|
|
281
|
+
check_headings "$file" '## Source' '## Observed Behavior' '## Expected Behavior' '## Acceptance Criteria' '## Classification' '## Related Issues'
|
|
282
|
+
acceptance_ids | grep -q . && rule_ok acceptance-criteria || rule_bad acceptance-criteria "missing AC checkbox"
|
|
283
|
+
value="$(state_value classification)"
|
|
284
|
+
case "$value" in VALID|AMBIGUOUS|ALREADY_FIXED|NOT_A_BUG|FEATURE) ;; *) rule_bad classification "invalid state value"; return;; esac
|
|
285
|
+
if grep -A100 '^## Classification$' "$file" 2>/dev/null | grep -Eq 'VALID|AMBIGUOUS|ALREADY_FIXED|NOT_A_BUG|FEATURE' && grep -A100 '^## Classification$' "$file" | grep -q "$value"; then rule_ok classification
|
|
286
|
+
else rule_bad classification "artifact does not match state"; fi
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
phase2() {
|
|
290
|
+
local file="$trace/02-reproduction.md"
|
|
291
|
+
check_headings "$file" '## Commands Tried' '## Reproduction Verdict'
|
|
292
|
+
grep -q '^```text$' "$file" 2>/dev/null && rule_ok reproduction-text-block || rule_bad reproduction-text-block "missing"
|
|
293
|
+
grep -Eq '^- Exit code: [0-9]+' "$file" 2>/dev/null && rule_ok reproduction-exit-code || rule_bad reproduction-exit-code "missing"
|
|
294
|
+
if is_already_fixed; then check_headings "$file" '## Fixing Change'; fi
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
phase25() {
|
|
298
|
+
local file="$trace/02-reproduction.md" header ac row found class check pre notes reason checkpoint manifest_path diff_path cells ac_probe
|
|
299
|
+
if is_already_fixed; then rule_ok obe-subset; return; fi
|
|
300
|
+
header='| AC | class | check | argv | expect | pre-fix | post-fix | notes |'
|
|
301
|
+
grep -Fx "$header" "$file" >/dev/null 2>&1 && rule_ok acceptance-table || { rule_bad acceptance-table "missing exact header"; return; }
|
|
302
|
+
while IFS= read -r ac; do
|
|
303
|
+
found="$(grep -E "^\\|[[:space:]]*$ac[[:space:]]*\\|" "$file" 2>/dev/null | wc -l | tr -d ' ')"
|
|
304
|
+
[ "$found" -eq 1 ] && rule_ok "acceptance-$ac" || rule_bad "acceptance-$ac" "must appear exactly once"
|
|
305
|
+
done < <(acceptance_ids)
|
|
306
|
+
check="$(grep -E '^\|[[:space:]]*AC[0-9]+[[:space:]]*\|' "$file" 2>/dev/null | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $4); print $4}' | sort | uniq -d | head -n1 || true)"
|
|
307
|
+
[ -z "$check" ] && rule_ok acceptance-check-ids || rule_bad acceptance-check-ids "duplicate check id $check"
|
|
308
|
+
while IFS= read -r row; do
|
|
309
|
+
cells="$(printf '%s' "$row" | awk -F'|' '{print NF}')"
|
|
310
|
+
if [ "${cells:-0}" -gt 10 ]; then
|
|
311
|
+
ac_probe="$(printf '%s' "$row" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')"
|
|
312
|
+
rule_bad "acceptance-table-row-${ac_probe:-unknown}" "row for ${ac_probe:-unknown} has too many columns (literal | in argv?)"
|
|
313
|
+
continue
|
|
314
|
+
fi
|
|
315
|
+
IFS='|' read -r _ ac class check argv expect pre post notes _ <<EOF
|
|
316
|
+
$row
|
|
317
|
+
EOF
|
|
318
|
+
ac="$(trim "$ac")"; class="$(trim "$class")"; check="$(trim "$check")"; pre="$(trim "$pre")"; notes="$(trim "$notes")"
|
|
319
|
+
case "$class" in
|
|
320
|
+
DISCRIMINATING) [ "$pre" = RED ] || rule_bad "pre-fix-$ac" "DISCRIMINATING must be RED"; [ -f "$trace/repro/$check.base.log" ] || rule_bad "base-log-$check" "missing" ;;
|
|
321
|
+
PRESERVING) [ "$pre" = GREEN ] || rule_bad "pre-fix-$ac" "PRESERVING must be GREEN"; [ -f "$trace/repro/$check.base.log" ] || rule_bad "base-log-$check" "missing" ;;
|
|
322
|
+
NEW-SURFACE) [ "$pre" = ERROR ] || rule_bad "pre-fix-$ac" "NEW-SURFACE must be ERROR"; [ -f "$trace/repro/$check.base.log" ] || rule_bad "base-log-$check" "missing" ;;
|
|
323
|
+
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
|
+
*) rule_bad "class-$ac" "invalid" ;;
|
|
325
|
+
esac
|
|
326
|
+
done < <(grep -E '^\|[[:space:]]*AC[0-9]+[[:space:]]*\|' "$file" 2>/dev/null || true)
|
|
327
|
+
check_headings "$file" '## Red checkpoint'
|
|
328
|
+
checkpoint="$(grep '^checkpoint-tree-id: ' "$file" 2>/dev/null | head -n1 | sed 's/^checkpoint-tree-id: //')"
|
|
329
|
+
is_hex "$checkpoint" && [ "$checkpoint" = "$(state_value checkpoint-tree-id)" ] && rule_ok red-checkpoint || rule_bad red-checkpoint "state binding missing or invalid"
|
|
330
|
+
manifest_path="$trace/repro/checkpoint.manifest"
|
|
331
|
+
# Header shape only - repro-check.sh owns full validation (row count, seq
|
|
332
|
+
# run, field count). The `rows=<N>` suffix is required, matching the fact
|
|
333
|
+
# that repro-check refuses a header with no count; awk rather than a
|
|
334
|
+
# `head | grep -q` pipeline so no SIGPIPE can decide the verdict, and the
|
|
335
|
+
# END guard makes an empty manifest fail instead of vacuously passing.
|
|
336
|
+
[ -f "$manifest_path" ] && awk 'NR == 1 { 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; }
|
|
337
|
+
diff_path="$(git diff-tree -r --name-only "$(state_value phase0-tree-id)" "$checkpoint" 2>/dev/null || true)"
|
|
338
|
+
while IFS= read -r check; do
|
|
339
|
+
[ -z "$check" ] && continue
|
|
340
|
+
awk -F '\t' -v p="$check" 'NR > 1 && $3 == p {found=1} END {exit !found}' "$manifest_path" && rule_ok "manifest-path-$check" || rule_bad "manifest-path-$check" "not recorded"
|
|
341
|
+
done <<EOF
|
|
342
|
+
$diff_path
|
|
343
|
+
EOF
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
phase3() {
|
|
347
|
+
local head tid
|
|
348
|
+
check_headings "$trace/05-fix-plan.md" '## Selected Fix' '## Candidate Fixes' '## Impact Analysis' '## Anticipated Defect-Class Sweep (Phase 4.2)'
|
|
349
|
+
check_headings "$trace/06-critic-review.md" '## Reviewed SHA / diff hash' '## Verdict' '## Check replay'
|
|
350
|
+
grep -Eq '^## Round [0-9]+$' "$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
|
+
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
|
+
[ -f "$trace/07-approved-plan.md" ] && rule_ok approved-plan || rule_bad approved-plan "missing"
|
|
353
|
+
# The checkpoint tree may be dirty relative to HEAD at Phase 3 (the fix is
|
|
354
|
+
# not implemented yet), so the plan-critic gate row is bound to the current
|
|
355
|
+
# HEAD commit and the current working-tree identity (tree_id), not the
|
|
356
|
+
# frozen checkpoint-tree-id.
|
|
357
|
+
head="$(git rev-parse HEAD)"; tid="$(tree_id)"
|
|
358
|
+
state_gate plan-critic APPROVE "$head" "$tid"
|
|
359
|
+
artifact_identity_matches_gate "$trace/06-critic-review.md" plan-critic "$head" "$tid"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
executable_ids() { grep -E '^\|[[:space:]]*AC[0-9]+[[:space:]]*\|' "$trace/02-reproduction.md" 2>/dev/null | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); gsub(/^[ \t]+|[ \t]+$/, "", $4); if ($3 != "NON-EXECUTABLE") print $4}'; }
|
|
363
|
+
phase4() {
|
|
364
|
+
local file="$trace/08-test-results.md" id
|
|
365
|
+
check_headings "$file" '## Regression Test' '## Acceptance check results' '## Quality Checks' '## Deferred-Work Scan' '## Verification Reasoning' '## Checkpoint verification'
|
|
366
|
+
while IFS= read -r id; do [ -z "$id" ] || { grep -Fx "### Check $id" "$file" >/dev/null 2>&1 && rule_ok "check-block-$id" || rule_bad "check-block-$id" "missing"; }; done < <(executable_ids)
|
|
367
|
+
if "$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
|
|
368
|
+
grep -A100 '^## Deferred-Work Scan$' "$file" 2>/dev/null | grep -q '^scan-deferred: clean' && rule_ok deferred-work-scan || rule_bad deferred-work-scan "clean result missing"
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
phase42() {
|
|
372
|
+
local file="$trace/08a-recurrence-sweep.md" hits rows
|
|
373
|
+
[ -f "$file" ] || { rule_bad recurrence-sweep "missing"; return; }
|
|
374
|
+
if grep -Eq '^no-defect-class: true$' "$file" 2>/dev/null; then
|
|
375
|
+
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
|
+
return
|
|
377
|
+
fi
|
|
378
|
+
check_headings "$file" '## Defect Class' '## Predicates and Results' '## Dispositions' '## Guardrail'
|
|
379
|
+
hits="$(grep -E '^- Predicate.*hits: [0-9]+' "$file" 2>/dev/null | sed -E 's/.*hits: ([0-9]+).*/\1/' | awk '{s += $1} END {print s + 0}')"
|
|
380
|
+
grep -Eq '^- Predicate.*hits: [0-9]+' "$file" 2>/dev/null && rule_ok recurrence-predicates || rule_bad recurrence-predicates "missing hit counts"
|
|
381
|
+
rows="$(awk '/^## Dispositions$/{in_table=1; next} /^## /{in_table=0} in_table && /^\|/ && $0 !~ /^\|[ -]*\|/ {n++} END {print n-1}' "$file")"
|
|
382
|
+
[ "$hits" -eq "$rows" ] && rule_ok recurrence-dispositions || rule_bad recurrence-dispositions "rows ($rows) do not equal hits ($hits)"
|
|
383
|
+
grep -A100 '^## Guardrail$' "$file" 2>/dev/null | tr '\n' ' ' | grep -Eq '### Check .*RED.*GREEN' && rule_ok recurrence-guardrail || rule_bad recurrence-guardrail "missing RED then GREEN check"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
phase45() {
|
|
387
|
+
clean_tree
|
|
388
|
+
local file="$trace/08b-implementation-review.md" head tid
|
|
389
|
+
check_headings "$file" '## Reviewed SHA / diff hash' '## Verdict' '## Independently re-run' '## Check integrity' '## Deferred / Scoped-Out / Unwired'
|
|
390
|
+
artifact_verdict_approved "$file" && rule_ok artifact-verdict-implementation-review || rule_bad artifact-verdict-implementation-review "must contain APPROVE under ## Verdict"
|
|
391
|
+
head="$(git rev-parse HEAD)"; tid="$(tree_id)"
|
|
392
|
+
state_gate implementation-review APPROVE "$head" "$tid"
|
|
393
|
+
artifact_identity_matches_gate "$file" implementation-review "$head" "$tid"
|
|
394
|
+
}
|
|
395
|
+
phase46() {
|
|
396
|
+
local ac file="$trace/09-final-critic.md" head tid
|
|
397
|
+
clean_tree
|
|
398
|
+
check_headings "$file" '## Reviewed SHA / diff hash' '## Verdict' '## Review Freshness' '## Deferred / Scoped-Out / Unwired' '## Acceptance criteria evidence'
|
|
399
|
+
artifact_verdict_approved "$file" && rule_ok artifact-verdict-final-critic || rule_bad artifact-verdict-final-critic "must contain APPROVE under ## Verdict"
|
|
400
|
+
head="$(git rev-parse HEAD)"; tid="$(tree_id)"
|
|
401
|
+
state_gate final-critic APPROVE "$head" "$tid"
|
|
402
|
+
artifact_identity_matches_gate "$file" final-critic "$head" "$tid"
|
|
403
|
+
while IFS= read -r ac; do grep -A100 '^## Acceptance criteria evidence$' "$file" 2>/dev/null | grep -q "$ac" && rule_ok "final-$ac" || rule_bad "final-$ac" "missing evidence"; done < <(acceptance_ids)
|
|
404
|
+
}
|
|
405
|
+
phase5() {
|
|
406
|
+
if is_already_fixed; then rule_ok obe-subset; return; fi
|
|
407
|
+
local file="$trace/10-pr-body.md" merge_value pr_head_line pr_head_sha
|
|
408
|
+
check_headings "$file" '## Acceptance Criteria -> Evidence' '## Waivers (or none)'
|
|
409
|
+
pr_head_line="$(grep -E '^PR head: [0-9a-f]{40}$' "$file" 2>/dev/null | head -n1 || true)"
|
|
410
|
+
if [ -z "$pr_head_line" ]; then
|
|
411
|
+
rule_bad pr-head "missing PR head: <40-hex> line"
|
|
412
|
+
else
|
|
413
|
+
pr_head_sha="${pr_head_line#PR head: }"
|
|
414
|
+
if [ "$pr_head_sha" = "$(git rev-parse HEAD)" ]; then rule_ok pr-head; else rule_bad pr-head "does not match HEAD"; fi
|
|
415
|
+
fi
|
|
416
|
+
merge_value="$(state_value merge)"
|
|
417
|
+
case "$merge_value" in
|
|
418
|
+
AWAITING_USER_APPROVAL|MERGED) rule_ok merge-state ;;
|
|
419
|
+
APPROVED:*) is_hex "${merge_value#APPROVED:}" && rule_ok merge-state || rule_bad merge-state "APPROVED: must be followed by a 40-hex sha" ;;
|
|
420
|
+
*) rule_bad merge-state "not ready" ;;
|
|
421
|
+
esac
|
|
422
|
+
}
|
|
423
|
+
merge_check() {
|
|
424
|
+
local file="$trace/10b-merge-approval.md" pr final
|
|
425
|
+
check_headings "$file" '## User approval (verbatim)' '## PR head SHA' '## Final critic reviewed-commit'
|
|
426
|
+
pr="$(grep -A3 '^## PR head SHA$' "$file" 2>/dev/null | grep -Eo '[0-9a-f]{40}' | head -n1 || true)"
|
|
427
|
+
final="$(grep -A3 '^## Final critic reviewed-commit$' "$file" 2>/dev/null | grep -Eo '[0-9a-f]{40}' | head -n1 || true)"
|
|
428
|
+
is_hex "$pr" && [ "$pr" = "$final" ] && rule_ok merge-sha-binding || rule_bad merge-sha-binding "PR and final critic SHA differ"
|
|
429
|
+
state_gate merge-approval RECORDED "" ""
|
|
430
|
+
echo 'NOTE: human-enforced gate; this validator checks presence and binding only'
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
command="${1:-}"; shift || true
|
|
434
|
+
case "$command" in
|
|
435
|
+
tree-id) [ "$#" -eq 0 ] || usage; tree_id; exit $? ;;
|
|
436
|
+
handshake) [ "$#" -eq 0 ] || usage; handshake; exit 0 ;;
|
|
437
|
+
phase) phase="${1:-}"; shift || true; case "$phase" in 0|1|2|2.5|3|4|4.2|4.5|4.6|5) ;; *) usage;; esac ;;
|
|
438
|
+
merge) phase="merge" ;;
|
|
439
|
+
*) usage ;;
|
|
440
|
+
esac
|
|
441
|
+
slug=""; trace=""
|
|
442
|
+
while [ "$#" -gt 0 ]; do
|
|
443
|
+
case "$1" in --slug) [ "$#" -ge 2 ] || usage; slug="$2"; shift 2;; --trace-dir) [ "$#" -ge 2 ] || usage; trace="$2"; shift 2;; *) usage;; esac
|
|
444
|
+
done
|
|
445
|
+
valid_slug "$slug" || { echo "trace-check: invalid slug" >&2; exit 2; }
|
|
446
|
+
[ -n "$trace" ] || trace="$root/.agents/issue-traces/$slug"
|
|
447
|
+
state="$trace/state.md"
|
|
448
|
+
if [ ! -d "$trace" ] || [ ! -f "$state" ]; then
|
|
449
|
+
echo "FAIL state: missing $state"
|
|
450
|
+
exit 1
|
|
451
|
+
fi
|
|
452
|
+
protocol_line="$(grep -c '^protocol: ' "$state" 2>/dev/null || true)"
|
|
453
|
+
protocol_value="$(state_value protocol)"
|
|
454
|
+
if [ "${protocol_line:-0}" -eq 0 ]; then
|
|
455
|
+
# No protocol line at all: legacy v2 ledger unless a v3-only key is present,
|
|
456
|
+
# in which case this is a v3 ledger that had its protocol line stripped.
|
|
457
|
+
if grep -Eq '^(phase0-tree-id|checkpoint-tree-id|handshake): ' "$state" 2>/dev/null; then
|
|
458
|
+
echo "FAIL state-protocol: missing (v3 ledger without protocol line)"
|
|
459
|
+
exit 1
|
|
460
|
+
fi
|
|
461
|
+
legacy=1
|
|
462
|
+
echo "WARN protocol: legacy trace (protocol missing), all failures downgraded to WARN"
|
|
463
|
+
elif [ "$protocol_value" != "3.0.0" ]; then
|
|
464
|
+
echo "FAIL state-protocol: unsupported $protocol_value"
|
|
465
|
+
exit 1
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
if [ "$phase" = merge ]; then merge_check
|
|
469
|
+
else
|
|
470
|
+
if is_already_fixed && [ "$phase" != 0 ] && [ "$phase" != 1 ] && [ "$phase" != 2 ]; then
|
|
471
|
+
rule_ok obe-subset
|
|
472
|
+
else
|
|
473
|
+
case "$phase" in
|
|
474
|
+
0) phase0;; 1) phase1;; 2) phase2;; 2.5) phase25;; 3) phase3;; 4) phase4;; 4.2) phase42;; 4.5) phase45;; 4.6) phase46;; 5) phase5;;
|
|
475
|
+
esac
|
|
476
|
+
fi
|
|
477
|
+
fi
|
|
478
|
+
[ "$failed" -eq 0 ] || exit 1
|
|
479
|
+
exit 0
|
|
@@ -116,24 +116,114 @@ fi
|
|
|
116
116
|
exclude_file="$git_dir/info/exclude"
|
|
117
117
|
entry='.agents/issue-traces/'
|
|
118
118
|
mkdir -p "$(dirname "$exclude_file")"
|
|
119
|
+
# Refuse to redirect into a pre-existing non-regular exclude-file path (e.g. a
|
|
120
|
+
# symlink). `[ ! -e ]` alone is not enough: a dangling symlink reports
|
|
121
|
+
# non-existent (dereferenced) while a bare `>>` still follows the link and
|
|
122
|
+
# writes through it to whatever it points at.
|
|
123
|
+
if [ -L "$exclude_file" ] || { [ -e "$exclude_file" ] && [ ! -f "$exclude_file" ]; }; then
|
|
124
|
+
echo "trace-init: refusing non-regular target: $exclude_file" >&2
|
|
125
|
+
exit 2
|
|
126
|
+
fi
|
|
119
127
|
if [ ! -f "$exclude_file" ] || ! grep -qxF "$entry" "$exclude_file" 2>/dev/null; then
|
|
120
128
|
printf '%s\n' "$entry" >> "$exclude_file"
|
|
121
129
|
fi
|
|
122
130
|
|
|
131
|
+
# Resolve the base identity before seeding state. Prefer the remote's declared
|
|
132
|
+
# default branch, then use the same conservative fallbacks as scan-deferred.
|
|
133
|
+
base_ref=""
|
|
134
|
+
if symbolic_ref="$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null)"; then
|
|
135
|
+
base_ref="origin/${symbolic_ref#refs/remotes/origin/}"
|
|
136
|
+
fi
|
|
137
|
+
if [ -z "$base_ref" ]; then
|
|
138
|
+
for candidate in origin/main origin/master main master; do
|
|
139
|
+
if git rev-parse --verify --quiet "$candidate^{commit}" >/dev/null 2>&1; then
|
|
140
|
+
base_ref="$candidate"
|
|
141
|
+
break
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
fi
|
|
145
|
+
base_sha="unset"
|
|
146
|
+
if [ -n "$base_ref" ]; then
|
|
147
|
+
base_sha="$(git rev-parse "$base_ref^{commit}")"
|
|
148
|
+
else
|
|
149
|
+
base_ref="unset"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Use a disposable index so the identity includes tracked and untracked source
|
|
153
|
+
# changes without touching the caller's real index. The trace directory is
|
|
154
|
+
# removed from the disposable index explicitly (same recipe as trace-check.sh
|
|
155
|
+
# tree-id), so it cannot affect the result even without the exclude entry.
|
|
156
|
+
tree_index="$(mktemp "${TMPDIR:-/tmp}/issue-tracer-index.XXXXXX")"
|
|
157
|
+
rm -f "$tree_index"
|
|
158
|
+
phase0_tree_id=""
|
|
159
|
+
if phase0_tree_id="$(GIT_INDEX_FILE="$tree_index" git -C "$root" read-tree HEAD && GIT_INDEX_FILE="$tree_index" git -C "$root" add -A -- . && GIT_INDEX_FILE="$tree_index" git -C "$root" rm -r --cached --ignore-unmatch -q -- .agents/issue-traces && GIT_INDEX_FILE="$tree_index" git -C "$root" write-tree)"; then
|
|
160
|
+
:
|
|
161
|
+
else
|
|
162
|
+
rm -f "$tree_index"
|
|
163
|
+
echo "trace-init: could not calculate phase-0 tree identity" >&2
|
|
164
|
+
exit 2
|
|
165
|
+
fi
|
|
166
|
+
rm -f "$tree_index"
|
|
167
|
+
|
|
123
168
|
# Seed state.md so the trail has a resumable starting point.
|
|
124
169
|
state_file="$trace_dir/state.md"
|
|
170
|
+
# Refuse to redirect into a pre-existing non-regular state.md path (e.g. a
|
|
171
|
+
# symlink). `[ ! -e ]` alone is not enough: a dangling symlink reports
|
|
172
|
+
# non-existent (dereferenced) while a bare `>` still follows the link and
|
|
173
|
+
# writes through it to whatever it points at.
|
|
174
|
+
if [ -L "$state_file" ] || { [ -e "$state_file" ] && [ ! -f "$state_file" ]; }; then
|
|
175
|
+
echo "trace-init: refusing non-regular target: $state_file" >&2
|
|
176
|
+
exit 2
|
|
177
|
+
fi
|
|
125
178
|
if [ ! -e "$state_file" ]; then
|
|
126
179
|
cat > "$state_file" <<EOF
|
|
127
180
|
# Trace State: $slug
|
|
181
|
+
protocol: 3.0.0
|
|
182
|
+
phase: 0
|
|
183
|
+
tier: unset
|
|
184
|
+
classification: unset
|
|
185
|
+
base-ref: $base_ref
|
|
186
|
+
base-sha: $base_sha
|
|
187
|
+
freshness: unset
|
|
188
|
+
phase0-tree-id: $phase0_tree_id
|
|
189
|
+
checkpoint-tree-id: unset
|
|
190
|
+
handshake: unset
|
|
191
|
+
tools: none
|
|
192
|
+
merge: not-applicable
|
|
193
|
+
next-action: unset
|
|
128
194
|
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
- Selected fix candidate:
|
|
133
|
-
- Unresolved risks:
|
|
134
|
-
- Next action:
|
|
195
|
+
## Gates
|
|
196
|
+
| gate | verdict | reviewed-commit | tree-id | artifact |
|
|
197
|
+
|---|---|---|---|---|
|
|
135
198
|
EOF
|
|
136
199
|
fi
|
|
137
200
|
|
|
201
|
+
repro_dir="$trace_dir/repro"
|
|
202
|
+
manifest_file="$repro_dir/checkpoint.manifest"
|
|
203
|
+
if [ -L "$repro_dir" ]; then
|
|
204
|
+
echo "trace-init: refusing to use 'repro/' - it is a symlink" >&2
|
|
205
|
+
exit 2
|
|
206
|
+
fi
|
|
207
|
+
mkdir -p "$repro_dir"
|
|
208
|
+
if [ -L "$repro_dir" ]; then
|
|
209
|
+
echo "trace-init: refusing to use 'repro/' - it is a symlink" >&2
|
|
210
|
+
exit 2
|
|
211
|
+
fi
|
|
212
|
+
# Refuse to redirect into a pre-existing non-regular manifest path (e.g. a
|
|
213
|
+
# symlink). `[ ! -e ]` alone is not enough: a broken symlink reports
|
|
214
|
+
# non-existent (dereferenced) while a bare `>` still follows the link and
|
|
215
|
+
# writes through it to whatever it points at.
|
|
216
|
+
if { [ -e "$manifest_file" ] && [ ! -f "$manifest_file" ]; } || [ -L "$manifest_file" ]; then
|
|
217
|
+
echo "trace-init: refusing non-regular target: $manifest_file" >&2
|
|
218
|
+
exit 2
|
|
219
|
+
fi
|
|
220
|
+
if [ ! -e "$manifest_file" ]; then
|
|
221
|
+
# The header carries the expected data-row count, restamped by every
|
|
222
|
+
# `repro-check.sh checkpoint` append. Seeding it without `rows=0` would make
|
|
223
|
+
# repro-check refuse the manifest: a header with no count is rejected there so it
|
|
224
|
+
# cannot be used to disable the count check (see validate_manifest).
|
|
225
|
+
printf '%s\n' '# issue-tracer checkpoint manifest v1 rows=0' > "$manifest_file"
|
|
226
|
+
fi
|
|
227
|
+
|
|
138
228
|
echo "trace-init: created $trace_dir"
|
|
139
229
|
echo "trace-init: ensured '$entry' in $exclude_file"
|
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
releaseCoderDispatchOwnership,
|
|
14
14
|
settleCoderDispatch,
|
|
15
15
|
transferCoderSettlementToBackground
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-7h94rfem.js";
|
|
17
17
|
import"./index-6chhqq2n.js";
|
|
18
18
|
import"./index-8bdk7124.js";
|
|
19
|
-
import"./index-
|
|
20
|
-
import"./index-
|
|
19
|
+
import"./index-e6n7cemz.js";
|
|
20
|
+
import"./index-p7hdp3rp.js";
|
|
21
21
|
import"./index-anxs69ad.js";
|
|
22
22
|
import"./index-z6xqpmqg.js";
|
|
23
23
|
import"./index-p612v1mv.js";
|
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
shouldRunOnStartup,
|
|
15
15
|
writeBackupArtifact,
|
|
16
16
|
writeDoctorArtifact
|
|
17
|
-
} from "./index-
|
|
17
|
+
} from "./index-k1ntjzmf.js";
|
|
18
18
|
import"./index-cw912re7.js";
|
|
19
19
|
import"./index-z6xqpmqg.js";
|
|
20
20
|
import"./index-ry16d01s.js";
|
|
21
|
-
import"./index-
|
|
21
|
+
import"./index-0e9jh9c7.js";
|
|
22
22
|
import"./index-a0d4fp6r.js";
|
|
23
23
|
import"./index-zjygnfay.js";
|
|
24
24
|
import"./index-bk5tah7q.js";
|