opencode-swarm 7.164.12 → 7.165.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.opencode/skills/durable-session-state/SKILL.md +1 -1
  2. package/.opencode/skills/issue-tracer/SKILL.md +140 -286
  3. package/.opencode/skills/issue-tracer/assets/pr-template.md +13 -7
  4. package/.opencode/skills/issue-tracer/references/acceptance-checks.md +66 -0
  5. package/.opencode/skills/issue-tracer/references/critic-gate.md +53 -11
  6. package/.opencode/skills/issue-tracer/references/evidence-artifacts.md +128 -23
  7. package/.opencode/skills/issue-tracer/references/full-resolution-contract.md +69 -0
  8. package/.opencode/skills/issue-tracer/references/install.md +31 -9
  9. package/.opencode/skills/issue-tracer/references/localization-playbook.md +26 -14
  10. package/.opencode/skills/issue-tracer/references/method-provenance.md +19 -3
  11. package/.opencode/skills/issue-tracer/references/phase-0-setup.md +90 -0
  12. package/.opencode/skills/issue-tracer/references/phase-1-intake.md +43 -0
  13. package/.opencode/skills/issue-tracer/references/untrusted-content.md +13 -5
  14. package/.opencode/skills/issue-tracer/scripts/repro-check.sh +416 -0
  15. package/.opencode/skills/issue-tracer/scripts/trace-check.sh +479 -0
  16. package/.opencode/skills/issue-tracer/scripts/trace-init.sh +96 -6
  17. package/.opencode/skills/loop/SKILL.md +1 -1
  18. package/.opencode/skills/{plan → swarm-plan}/SKILL.md +1 -1
  19. package/dist/cli/{coder-settlement-yh1q5fr3.js → coder-settlement-89nt4j7g.js} +11 -11
  20. package/dist/cli/{config-doctor-bd4yzmh7.js → config-doctor-gm6f5chm.js} +3 -3
  21. package/dist/cli/{core-y0s1vmyc.js → core-91p43zwj.js} +2 -2
  22. package/dist/cli/{curation-policy-gs1qakn7.js → curation-policy-bqpmry2n.js} +5 -5
  23. package/dist/cli/{curator-brqack0j.js → curator-4bs2hmy9.js} +36 -36
  24. package/dist/cli/{curator-drift-1sg9yk2m.js → curator-drift-hbc3w5zj.js} +4 -4
  25. package/dist/cli/curator-llm-factory-fhbjwar0.js +70 -0
  26. package/dist/cli/{evidence-summary-service-nqbcvp30.js → evidence-summary-service-g1xd8egd.js} +16 -16
  27. package/dist/cli/{gate-evidence-9stdtpej.js → gate-evidence-gdmgsz7c.js} +7 -7
  28. package/dist/cli/guardrail-explain-1nfbtpgg.js +71 -0
  29. package/dist/cli/{guardrail-log-sf39x00b.js → guardrail-log-nyggdv04.js} +8 -8
  30. package/dist/cli/{guardrail-reset-9wr6a60s.js → guardrail-reset-rp9jq0gj.js} +36 -36
  31. package/dist/cli/{hive-promoter-bqtj8261.js → hive-promoter-39xb4tx2.js} +36 -36
  32. package/dist/cli/{index-cgewrpw5.js → index-0e9jh9c7.js} +4 -3
  33. package/dist/cli/{index-3fnym6ac.js → index-1v6vc90w.js} +4 -4
  34. package/dist/cli/{index-hg09xemc.js → index-2k53pfnc.js} +4 -4
  35. package/dist/cli/{index-wknpkfm8.js → index-3xyjvx24.js} +7 -7
  36. package/dist/cli/{index-03enxdaq.js → index-4gmt79q9.js} +1 -1
  37. package/dist/cli/{index-qgajv9rd.js → index-58be2gtd.js} +3 -3
  38. package/dist/cli/{index-r2p0pgst.js → index-5ebgzb5a.js} +3 -3
  39. package/dist/cli/{index-k0s8n6m5.js → index-5ythtd9t.js} +11 -11
  40. package/dist/cli/{index-hb7bh4r2.js → index-6be5caj8.js} +30 -6
  41. package/dist/cli/{index-yvqg6kn5.js → index-6m24v15v.js} +2 -2
  42. package/dist/cli/{index-sphahrmq.js → index-6rx1rdaq.js} +1 -1
  43. package/dist/cli/{index-r69gzes2.js → index-7h94rfem.js} +7 -7
  44. package/dist/cli/{index-xxfk62zd.js → index-8bdk7124.js} +2 -2
  45. package/dist/cli/{index-50j914b5.js → index-a0d4fp6r.js} +29 -7
  46. package/dist/cli/{index-8vw2pjgr.js → index-afpkqsdb.js} +6 -6
  47. package/dist/cli/{index-4mjmnysc.js → index-ajknebrc.js} +1 -1
  48. package/dist/cli/{index-qqq04n70.js → index-anxs69ad.js} +1 -1
  49. package/dist/cli/{index-888pq4xx.js → index-c4ddtrqg.js} +7 -7
  50. package/dist/cli/{index-ztm0q26v.js → index-dh5th4jn.js} +1 -1
  51. package/dist/cli/{index-c45662wc.js → index-e45htt92.js} +1 -1
  52. package/dist/cli/{index-ad9064j9.js → index-e6n7cemz.js} +4 -4
  53. package/dist/cli/{index-hvz4ad2t.js → index-f36hy1x4.js} +1 -1
  54. package/dist/cli/{index-40twgsyh.js → index-g8wdekzz.js} +2 -2
  55. package/dist/cli/{index-0byep4pe.js → index-gn6ybsc4.js} +8 -8
  56. package/dist/cli/{index-ewv8qa8q.js → index-gy0nyfbw.js} +2 -2
  57. package/dist/cli/{index-wasdrhev.js → index-hdakb4n9.js} +4 -4
  58. package/dist/cli/{index-5wc5pbzc.js → index-hwz46jj7.js} +682 -349
  59. package/dist/cli/{index-eed0gkv6.js → index-j2svfhzk.js} +2 -2
  60. package/dist/cli/{index-2bhz5k91.js → index-jre6tm3v.js} +2 -2
  61. package/dist/cli/{index-b25t91a3.js → index-k1ntjzmf.js} +2 -2
  62. package/dist/cli/{index-kxv193aq.js → index-n3pjcks8.js} +5 -5
  63. package/dist/cli/{index-ebce775n.js → index-p7hdp3rp.js} +2 -2
  64. package/dist/cli/{index-d3mpxsmy.js → index-ptsqnxwd.js} +7 -7
  65. package/dist/cli/{index-d7j3n5ra.js → index-qzvry1n1.js} +41 -39
  66. package/dist/cli/{index-m8xrbdmn.js → index-r5hj3nfe.js} +2 -2
  67. package/dist/cli/{index-x39hjdf1.js → index-tqw8w2n5.js} +1 -1
  68. package/dist/cli/{index-es64amqt.js → index-w6988bbc.js} +1 -1
  69. package/dist/cli/{index-mee9maf5.js → index-xwbn7c8m.js} +2 -2
  70. package/dist/cli/{index-4vb56w2k.js → index-y1zhpfk8.js} +4 -4
  71. package/dist/cli/{index-vw66q1rw.js → index-zycp24b9.js} +1 -1
  72. package/dist/cli/index.js +146 -67
  73. package/dist/cli/{knowledge-escalator-01vva51d.js → knowledge-escalator-eq07pcj3.js} +13 -13
  74. package/dist/cli/{knowledge-events-dpfwh1hs.js → knowledge-events-rv9q9mkm.js} +11 -11
  75. package/dist/cli/{knowledge-link-2rvp966j.js → knowledge-link-dgvbqjqg.js} +4 -4
  76. package/dist/cli/{knowledge-store-aw58ae7b.js → knowledge-store-cxcr0ttk.js} +5 -5
  77. package/dist/cli/{knowledge-validator-yhd31ryf.js → knowledge-validator-8qtd9ys0.js} +7 -7
  78. package/dist/cli/{model-preflight-288nsgpw.js → model-preflight-3s1c0zpc.js} +1 -1
  79. package/dist/cli/{pending-delegations-fjetcz8e.js → pending-delegations-dp3g8e7k.js} +11 -11
  80. package/dist/cli/{pr-subscriptions-cwwtfs8w.js → pr-subscriptions-3vx06rar.js} +11 -11
  81. package/dist/cli/{pr-workflow-gate-5xwtg1k2.js → pr-workflow-gate-bbet0apk.js} +36 -36
  82. package/dist/cli/{runner-mb51twxt.js → runner-0e3bjeev.js} +5 -5
  83. package/dist/cli/{scan-cursor-j11qwf0v.js → scan-cursor-zwm0xng7.js} +6 -6
  84. package/dist/cli/{schema-0xp3bde3.js → schema-ebfxgya0.js} +2 -2
  85. package/dist/cli/{scope-persistence-brrk806y.js → scope-persistence-6c55akfz.js} +15 -15
  86. package/dist/cli/{skill-generator-j8qzmet9.js → skill-generator-mv5jxzsh.js} +15 -15
  87. package/dist/cli/{snapshot-coordination-init-h0xmtftq.js → snapshot-coordination-init-n556wa18.js} +36 -36
  88. package/dist/cli/{telemetry-m84ap0pp.js → telemetry-a1yvjerj.js} +1 -1
  89. package/dist/cli/{worktree-collision-ownership-j9zykj18.js → worktree-collision-ownership-pp4z4z8p.js} +12 -12
  90. package/dist/cli/{worktree-isolation-jd39xz6h.js → worktree-isolation-69hz9dgg.js} +36 -36
  91. package/dist/commands/benchmark.d.ts +5 -1
  92. package/dist/commands/registry.d.ts +34 -43
  93. package/dist/commands/turbo.d.ts +1 -1
  94. package/dist/config/bundled-skills.d.ts +4 -3
  95. package/dist/config/constants.d.ts +13 -0
  96. package/dist/config/skill-mirrors.d.ts +3 -3
  97. package/dist/hooks/extractors.d.ts +9 -0
  98. package/dist/hooks/non-architect-advisory.d.ts +18 -0
  99. package/dist/hooks/repo-graph-injection.d.ts +16 -0
  100. package/dist/index.js +73 -71
  101. package/dist/services/decision-drift-analyzer.d.ts +7 -0
  102. package/dist/services/handoff-service.d.ts +10 -0
  103. package/dist/services/status-service.d.ts +27 -0
  104. package/dist/state.d.ts +8 -0
  105. package/dist/tools/repo-graph/builder.d.ts +1 -25
  106. package/dist/tools/repo-graph/cache.d.ts +5 -1
  107. package/dist/tools/repo-graph/fallback-scanner.d.ts +71 -0
  108. package/dist/tools/repo-graph/indexed-storage.d.ts +11 -0
  109. package/dist/tools/repo-graph/storage.d.ts +7 -0
  110. package/dist/utils/context-decisions.d.ts +77 -0
  111. package/dist/utils/sanitize-display.d.ts +7 -0
  112. package/package.json +3 -2
  113. package/dist/cli/curator-llm-factory-dtsjc3zf.js +0 -70
  114. package/dist/cli/guardrail-explain-3ff1z3fm.js +0 -71
@@ -0,0 +1,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
- - Phase: 0 (setup)
130
- - Completed gates:
131
- - Active hypothesis:
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"
@@ -134,7 +134,7 @@ directly (skip full discovery dialogue) — the objective is already framed.
134
134
  1. Load `file:.swarm/bundled-skills/pre-phase-briefing/SKILL.md` (required before
135
135
  planning, especially on cycle 2+: it reads the prior retrospective and
136
136
  verifies codebase reality so the new plan reflects what actually changed).
137
- 2. Load `file:.swarm/bundled-skills/plan/SKILL.md` to decompose the work into
137
+ 2. Load `file:.swarm/bundled-skills/swarm-plan/SKILL.md` to decompose the work into
138
138
  tasks and call `save_plan`. With `depth=exhaustive`, prefer finer task
139
139
  granularity and deeper localization.
140
140
  3. Load `file:.swarm/bundled-skills/critic-gate/SKILL.md` to put the plan through
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: plan
2
+ name: swarm-plan
3
3
  audience: swarm-plugin
4
4
  description: >
5
5
  Full execution protocol for MODE: PLAN -- plan creation, external plan ingestion, QA gate persistence, task granularity, and traceability checks.