@oneie/claude 0.8.0 → 0.10.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 (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/abm-strategist.md +67 -1
  3. package/agents/ads-meta.md +67 -1
  4. package/agents/analyst.md +67 -1
  5. package/agents/animator.md +108 -0
  6. package/agents/architect.md +269 -20
  7. package/agents/brand-guardian.md +67 -1
  8. package/agents/brand-strategist.md +67 -1
  9. package/agents/campaign-content.md +67 -1
  10. package/agents/campaign-email.md +67 -1
  11. package/agents/campaign-sms.md +67 -1
  12. package/agents/campaign-social.md +67 -1
  13. package/agents/cco.md +83 -2
  14. package/agents/ceo.md +108 -11
  15. package/agents/chairman.md +197 -0
  16. package/agents/cmo.md +82 -2
  17. package/agents/community-greeter.md +67 -1
  18. package/agents/community-moderator.md +67 -1
  19. package/agents/compliance.md +67 -1
  20. package/agents/copywriter.md +67 -1
  21. package/agents/creative-strategist.md +67 -1
  22. package/agents/cro.md +81 -1
  23. package/agents/cto.md +266 -28
  24. package/agents/customer-interviewer.md +67 -1
  25. package/agents/customer-researcher.md +67 -1
  26. package/agents/customer-success-manager.md +67 -1
  27. package/agents/customer-trainer.md +67 -1
  28. package/agents/cxo.md +82 -1
  29. package/agents/demand-creator.md +67 -1
  30. package/agents/demo-mover.md +67 -1
  31. package/agents/demo-specialist.md +67 -1
  32. package/agents/demo-thai-family-law.md +67 -1
  33. package/agents/designer.md +67 -1
  34. package/agents/discovery-caller.md +67 -1
  35. package/agents/doctor.md +269 -0
  36. package/agents/educate-coach.md +67 -1
  37. package/agents/elevate-tutor.md +67 -1
  38. package/agents/email-lifecycle-marketer.md +67 -1
  39. package/agents/engage-specialist.md +67 -1
  40. package/agents/events-coordinator.md +67 -1
  41. package/agents/foundation-builder.md +67 -1
  42. package/agents/funnel-architect.md +67 -1
  43. package/agents/gift-creator.md +67 -1
  44. package/agents/google-ads.md +67 -1
  45. package/agents/guide.md +67 -1
  46. package/agents/helpdesk-dispatcher.md +67 -1
  47. package/agents/hook-specialist.md +67 -1
  48. package/agents/identify-optimizer.md +67 -1
  49. package/agents/implementer.md +313 -45
  50. package/agents/incident-commander.md +67 -1
  51. package/agents/insights-lead.md +87 -1
  52. package/agents/journey-runner.md +67 -1
  53. package/agents/linkedin-ads.md +67 -1
  54. package/agents/live-sales-chat.md +67 -1
  55. package/agents/market-researcher.md +67 -1
  56. package/agents/media-buyer.md +67 -1
  57. package/agents/memory-keeper.md +195 -0
  58. package/agents/movers-customer-researcher.md +67 -1
  59. package/agents/movers-foundation-builder.md +67 -1
  60. package/agents/movers-market-researcher.md +67 -1
  61. package/agents/movers-pricing-strategist.md +67 -1
  62. package/agents/nurture-architect.md +67 -1
  63. package/agents/offer-architect.md +67 -1
  64. package/agents/onboarder.md +67 -1
  65. package/agents/onboarding-specialist.md +67 -1
  66. package/agents/operations-dashboard.md +87 -1
  67. package/agents/perf-engineer.md +333 -37
  68. package/agents/playbook-writer.md +67 -1
  69. package/agents/plg-strategist.md +67 -1
  70. package/agents/positioning-architect.md +67 -1
  71. package/agents/press-officer.md +67 -1
  72. package/agents/pricing-strategist.md +67 -1
  73. package/agents/privacy-officer.md +67 -1
  74. package/agents/referral-manager.md +67 -1
  75. package/agents/refine-analyst.md +67 -1
  76. package/agents/release-manager.md +446 -39
  77. package/agents/renewals-upsell-rep.md +67 -1
  78. package/agents/review-engineer.md +319 -45
  79. package/agents/rewards-steward.md +67 -1
  80. package/agents/sales-call-coach.md +67 -1
  81. package/agents/sales-closer.md +67 -1
  82. package/agents/security-auditor.md +343 -48
  83. package/agents/sell-closer.md +67 -1
  84. package/agents/share-amplifier.md +67 -1
  85. package/agents/social-media-manager.md +67 -1
  86. package/agents/storyteller.md +301 -0
  87. package/agents/strategist.md +67 -1
  88. package/agents/strategy-aligner.md +67 -1
  89. package/agents/support-agent.md +67 -1
  90. package/agents/tagger.md +327 -0
  91. package/agents/tech-writer.md +195 -22
  92. package/agents/test-engineer.md +398 -29
  93. package/agents/tiktok-ads.md +67 -1
  94. package/agents/tracking-engineer.md +67 -1
  95. package/agents/trailkeeper.md +181 -0
  96. package/agents/upsell-strategist.md +67 -1
  97. package/agents/voice.md +67 -1
  98. package/agents/w1-recon.md +1 -1
  99. package/agents/w2-decide.md +1 -1
  100. package/agents/w3-edit.md +8 -2
  101. package/agents/w4-verify.md +13 -0
  102. package/agents/workflow-optimiser.md +81 -1
  103. package/commands/close.md +916 -160
  104. package/commands/deploy.md +102 -724
  105. package/commands/do.md +58 -2
  106. package/commands/sweep.md +159 -0
  107. package/commands/tasks.md +222 -0
  108. package/hooks/scripts/dev-only.sh +135 -0
  109. package/hooks/scripts/git-add-guard.sh +37 -2
  110. package/hooks/scripts/session-start.sh +32 -4
  111. package/package.json +1 -1
  112. package/rules/scripts.md +85 -0
  113. package/scripts/CLAUDE.md +315 -0
  114. package/scripts/ad-copy-lint.sh +656 -0
  115. package/scripts/agent-actor-parity.sh +129 -0
  116. package/scripts/blocks-manifest-cached.sh +100 -0
  117. package/scripts/chat-context-check.sh +89 -0
  118. package/scripts/chrome.mjs +18 -0
  119. package/scripts/close-metrics.sh +587 -0
  120. package/scripts/close-owner.sh +326 -0
  121. package/scripts/db-sync-lock-check.sh +116 -0
  122. package/scripts/deploy-emit.sh +311 -0
  123. package/scripts/deploy-gate-check.sh +155 -0
  124. package/scripts/deploy-ready.sh +78 -0
  125. package/scripts/deploy-record.sh +605 -0
  126. package/scripts/deploy-schema-check.sh +58 -0
  127. package/scripts/deploy.sh +393 -243
  128. package/scripts/do-auto.sh +127 -26
  129. package/scripts/do-board.sh +429 -0
  130. package/scripts/do-close.sh +1184 -0
  131. package/scripts/do-consumer-sweep.sh +18 -1
  132. package/scripts/do-decide.sh +476 -0
  133. package/scripts/do-fleet.sh +8 -2
  134. package/scripts/do-plan-json.mjs +110 -12
  135. package/scripts/do-prove-selftest.sh +108 -0
  136. package/scripts/do-prove.sh +86 -10
  137. package/scripts/do-rank.py +200 -3
  138. package/scripts/do-reconcile.sh +73 -12
  139. package/scripts/do-signal.sh +101 -23
  140. package/scripts/do-smoke.sh +18 -1
  141. package/scripts/do-w4-gates.sh +11 -1
  142. package/scripts/do-world-check.sh +153 -0
  143. package/scripts/download-stats.sh +172 -0
  144. package/scripts/factory-brief-check.sh +330 -0
  145. package/scripts/factory-check.sh +18 -1
  146. package/scripts/factory-close-check.sh +257 -0
  147. package/scripts/factory-emit.sh +211 -0
  148. package/scripts/factory-executor-check.mjs +353 -0
  149. package/scripts/factory-peak.sh +301 -0
  150. package/scripts/factory-repo.sh +71 -0
  151. package/scripts/factory-review-check.mjs +61 -0
  152. package/scripts/factory-tasks-check.sh +18 -1
  153. package/scripts/fixtures/factory-brief-real.md +44 -0
  154. package/scripts/flywheel-outcome.sh +63 -0
  155. package/scripts/gate-reaper-check.sh +98 -0
  156. package/scripts/gate-reaper.sh +9 -0
  157. package/scripts/gate-watchdog.sh +619 -0
  158. package/scripts/gc-content-check.sh +142 -0
  159. package/scripts/gh-traffic-capture.sh +153 -0
  160. package/scripts/govern-order-check.sh +202 -0
  161. package/scripts/governor-doors-check.sh +86 -5
  162. package/scripts/health.sh +448 -0
  163. package/scripts/id-inventory.mjs +418 -0
  164. package/scripts/incident.sh +212 -0
  165. package/scripts/land.sh +755 -45
  166. package/scripts/lib/gc-finished.sh +77 -0
  167. package/scripts/livekit-ratchet.sh +18 -1
  168. package/scripts/machine-check.sh +1 -1
  169. package/scripts/memory-index-budget.sh +79 -0
  170. package/scripts/npm-downloads.sh +109 -0
  171. package/scripts/one-agents.mjs +204 -8
  172. package/scripts/one-resume.sh +31 -3
  173. package/scripts/pr-body.sh +335 -0
  174. package/scripts/preview-fd-check.sh +289 -0
  175. package/scripts/redirect-lint.sh +169 -0
  176. package/scripts/release.sh +40 -6
  177. package/scripts/resume-lost-sessions.sh +68 -0
  178. package/scripts/shoot-pages.mjs +140 -0
  179. package/scripts/signal-meta-backfill.ts +451 -0
  180. package/scripts/signal-watch.sh +63 -6
  181. package/scripts/speed-cache-check.sh +12 -2
  182. package/scripts/sweep.sh +426 -0
  183. package/scripts/task-titles-dump.ts +101 -0
  184. package/scripts/test-cached.sh +47 -10
  185. package/scripts/test-lanes.sh +14 -0
  186. package/scripts/thread-name-backfill.ts +215 -0
  187. package/scripts/triage-shape-check.sh +149 -0
  188. package/scripts/tsc-cached.sh +155 -8
  189. package/scripts/typedb-flake-check.sh +3 -1
  190. package/scripts/urls-lint.sh +8 -0
  191. package/scripts/verify-board-doors.sh +80 -0
  192. package/scripts/verify-fast.sh +159 -6
  193. package/scripts/worktree-up.sh +21 -3
  194. package/skills/astro/SKILL.md +9 -3
  195. package/skills/astro/optimize-performance.md +3 -2
  196. package/skills/cloudflare/SKILL.md +3 -2
  197. package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
  198. package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
  199. package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
  200. package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  201. package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  202. package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  203. package/skills/cloudflare-security-audit/HUNTING.md +251 -0
  204. package/skills/cloudflare-security-audit/LICENSE +21 -0
  205. package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  206. package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  207. package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
  208. package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
  209. package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  210. package/skills/cloudflare-security-audit/SKILL.md +192 -0
  211. package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  212. package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  213. package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  214. package/skills/cloudflare-security-audit/report-schema.json +461 -0
  215. package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
  216. package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
  217. package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
  218. package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
  219. package/skills/deploy/REFERENCE.md +713 -0
  220. package/skills/deploy/SKILL.md +140 -0
  221. package/skills/fleet-audit/SKILL.md +58 -0
  222. package/skills/meeting/SKILL.md +220 -0
  223. package/skills/planning/SKILL.md +256 -0
  224. package/skills/shadcn/SKILL.md +1 -1
  225. package/skills/typedb/SKILL.md +7 -0
  226. package/skills/voice/SKILL.md +94 -6
  227. package/skills/voice/corpus-check.sh +87 -0
  228. package/templates/template-agent.md +7 -1
  229. package/templates/template-feature.md +9 -0
  230. package/templates/template-todo.md +29 -0
@@ -0,0 +1,1184 @@
1
+ #!/usr/bin/env bash
2
+ # do-close.sh — the universal end-of-workflow closer. Fast, bounded, honest.
3
+ #
4
+ # manifest: needs-env
5
+ # Reads GATEWAY_API_KEY / SERVER_SECRET through ONE_ENV_FILE (default
6
+ # one.ie/web/.env plus the monorepo fallbacks), exactly as do-signal.sh does.
7
+ #
8
+ # WHY IT EXISTS. `/close` was nine markdown steps a model walked by hand: read the
9
+ # rubric weights, POST four doors, settle the promise, propagate docs, write a
10
+ # report. Every step was deterministic and every step could hang — none of the
11
+ # curls it prescribed carried a timeout, so ONE dead door made the whole close
12
+ # read as a stall, and the usual repair was to skip the close entirely. A close
13
+ # that is skipped is Rule 1 broken: the signal never closes and the paths never
14
+ # learn. So the close is now a script, it is BOUNDED, and what it cannot do it
15
+ # FILES rather than drops.
16
+ #
17
+ # THE CONTRACT, in one line: close everything closable in one bounded pass; for
18
+ # everything else, leave a row that names the exact command that finishes it.
19
+ #
20
+ # THE HONESTY LAW (inherited verbatim from do-w4-gates.sh — same four states):
21
+ # pass : ran, exit 0
22
+ # fail : ran, non-zero — the close is real and the verdict is bad
23
+ # unrun : could not run inside its budget, or its precondition was not met
24
+ # n/a : does not apply to this close (by rule, not by accident)
25
+ # An unrun leg is NOT a closed leg. `closed` is true only when there are ZERO
26
+ # fails and ZERO unruns. Every non-pass leg gets a ledger row and, where the
27
+ # board is reachable, a substrate task whose notes ARE the command to finish it.
28
+ #
29
+ # THE ONE DEPENDENCY EDGE. `task` (the board write that says done) depends on
30
+ # `gate`. Decoupling them rebuilds this repo's most expensive scar — cycles
31
+ # closed green at 0.92 with "tsc not measured, box saturated". A close with no
32
+ # receipt does not write `done`; it writes a row saying which command mints one.
33
+ #
34
+ # WHY IT NEVER RUNS THE SUITE. `/close` is a FULL-lane trigger and
35
+ # "a fast pass is never reported as a full pass" is locked. This script does not
36
+ # resolve that by swapping in the fast lane — it asks test-cached.sh whether THIS
37
+ # EXACT TREE already carries a full-suite receipt (TEST_CACHE_KEY_ONLY=1 prints
38
+ # the key, the stamp's presence is the answer, and the key is content-addressed
39
+ # so a neighbour's suite counts as yours). A memo HIT is free and it counts. No
40
+ # receipt is `unrun`, filed, and named — never a fast pass wearing a full badge.
41
+ #
42
+ # USAGE
43
+ # do-close.sh [<slug>] [options] close a slug / a workflow's tail
44
+ # do-close.sh --pending the legs nothing has closed (exit 6 if any)
45
+ # do-close.sh --self-test fixtures + RED PROOFS
46
+ #
47
+ # OPTIONS
48
+ # --tid <tid> substrate task id to set status on
49
+ # --workspace <slug> board workspace (default: one)
50
+ # --status <s> done|failed|dissolved|timeout (default: done)
51
+ # --composite <0.NN> cycle rubric composite (default: unset)
52
+ # --edge <from→to> path the marks land on (default: close:<slug>→result)
53
+ # --dims k=v,... security,stability,simplicity,speed[,integration]
54
+ # --tags a,b tags on the feedback signal
55
+ # --only <leg,leg> run only these legs — the retry door
56
+ # --budget <secs> whole-run wall clock (default: 45)
57
+ # --leg-budget <secs> per-leg wall clock (default: 12)
58
+ # --notify always ping the human, not only on an unfiled leg
59
+ # --no-file do not file board tasks (ledger only)
60
+ # --strict exit 1 when not fully closed (default: always exit 0)
61
+ # --json machine-readable receipt on stdout
62
+ # --no-metrics legs + verdict only, no measured block
63
+ # --reply ALSO fold the enriched envelope back to the previous
64
+ # sender (CLOSE_SIGNAL_SENDER, else the origin)
65
+ # --no-decide file every open leg, decide nothing (the pre-2026-09-09
66
+ # behaviour — every blocker waits for a person)
67
+ # --dry-run plan the legs, touch nothing
68
+ #
69
+ # EXIT
70
+ # 0 ran and reported — the DEFAULT even when legs are open, because a closer
71
+ # that fails its caller is a closer callers stop calling. `--strict` opts in.
72
+ # 1 --strict and not fully closed
73
+ # 6 --pending and rows are open
74
+ # 2 usage
75
+ #
76
+ # END OF EVERY WORKFLOW. Call it last, wrapped: `bash .claude/scripts/do-close.sh
77
+ # <slug> || true`. It is idempotent — the board writes dedupe, the ledger is
78
+ # keyed by <slug>:<leg>, and a leg that already passed is skipped on a re-run.
79
+ set -uo pipefail # deliberately NOT -e: a closer must finish its report
80
+
81
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
82
+ # CLOSE_LEDGER overrides the path — the self-test drives a sandbox ledger with it,
83
+ # and it MUST be resolved here, above --pending: an override applied later meant
84
+ # --pending read the real ledger while the run wrote the sandbox one, and the
85
+ # ledger red-proof passed on rows a previous check had left behind.
86
+ # THE LEDGER LIVES IN THE PRIMARY WORKTREE, never in $ROOT. `release.sh ship`
87
+ # runs deploy.sh from `.release/`, so $ROOT there is `.release` — a tree the next
88
+ # promote re-materialises away. Rows written there are invisible to `--pending`
89
+ # from main, on exactly the path where "files what it cannot close" matters most.
90
+ # `--git-common-dir` resolves to the primary tree's .git from any linked worktree;
91
+ # it is the same pin deploy-record.sh makes for the deploy record.
92
+ _primary_root() {
93
+ local gcd
94
+ gcd="$(git -C "$ROOT" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || { printf '%s' "$ROOT"; return; }
95
+ [ -n "$gcd" ] && printf '%s' "$(dirname "$gcd")" || printf '%s' "$ROOT"
96
+ }
97
+ LEDGER="${CLOSE_LEDGER:-$(_primary_root)/.claude/.close-pending.jsonl}"
98
+ # MUST match the WRITER's default byte for byte. test-cached.sh:32 writes to
99
+ # ${TMPDIR:-/tmp}/one-test-cache and release.sh:56 reads the same; this file
100
+ # defaulted to $ROOT/.claude/.test-cache — a directory nothing creates and
101
+ # nothing writes. So `gate` answered "0/2 lane(s) stamped" on a tree whose
102
+ # receipts were sitting on disk, and because `task` is vetoed behind `gate`
103
+ # (the board write must never be decided around its gate), the board could
104
+ # never be told `done` by this closer at all.
105
+ #
106
+ # --self-test did not catch it and structurally could not: both gate probes set
107
+ # TEST_CACHE_DIR="$(mktemp -d)" explicitly, so the broken DEFAULT is the one
108
+ # path the red proof never exercises. A check that stubs the seam it is meant
109
+ # to guard proves the stub works.
110
+ CACHE_DIR="${TEST_CACHE_DIR:-${TMPDIR:-/tmp}/one-test-cache}"
111
+
112
+ # THE CLOSED LOOP ON THE DECISIONS THEMSELVES (Rule 1, applied one level up).
113
+ # A decision taken is not a decision proven. What licenses the NEXT auto-decision
114
+ # is what happened to the leg AFTER this one acted — so the previous run's
115
+ # `decided` rows are read here, before this run rewrites them, and settled below
116
+ # against this run's leg states. mark on pass, warn on still-open. Reading the
117
+ # command's exit code instead would measure the command, not the choice.
118
+ PRIOR_DECIDED=""
119
+ if [ -f "$LEDGER" ]; then
120
+ PRIOR_DECIDED=$(grep -o '"k":"[^"]*","at[^}]*"decide":"taken:[a-z-]*"' "$LEDGER" 2>/dev/null \
121
+ | sed -n 's/"k":"[^:]*:\([a-z]*\)".*"decide":"taken:\([a-z-]*\)"/\1 \2/p' || true)
122
+ fi
123
+
124
+ ONE_ENV_FILE="${ONE_ENV_FILE:-${DO_ENV_FILE:-one.ie/web/.env}}"
125
+ _MONOREPO_ENV_CANDIDATES="$ROOT/one.ie/web/.env $ROOT/one.ie/web/.dev.vars"
126
+
127
+ SLUG=""; TID=""; WORKSPACE="${CC_TASKS_WORKSPACE:-one}"; STATUS="done"
128
+ COMPOSITE=""; EDGE=""; DIMS=""; TAGS=""; ONLY=""
129
+ BUDGET="${CLOSE_BUDGET:-45}"; LEG_BUDGET="${CLOSE_LEG_BUDGET:-12}"
130
+ NOTIFY=0; NOFILE=0; STRICT=0; JSON=0; DRY=0; PENDING=0; SELFTEST=0; METRICS=1
131
+ # The decide layer is ON by default and that is the point: a blocker whose answer
132
+ # the substrate has already watched come good five times is not a question. It
133
+ # still cannot act under --dry-run (structural, do-decide.sh check 9) and it can
134
+ # never touch a vetoed fork.
135
+ DECIDE=1
136
+ # DECIDE_BIN exists for ONE reason: --self-test drives a gutted COPY of the real
137
+ # do-decide.sh (its `_verdict` short-circuited to `highway`) so the taken path is
138
+ # proven without writing five fake marks onto a live decision path. Seeding the
139
+ # oracle to test the reader would make the test the evidence.
140
+ DECIDE_BIN="${DECIDE_BIN:-$ROOT/.claude/scripts/do-decide.sh}"
141
+ # The return leg is OPT-IN. A close with no caller has nobody to reply to, and a
142
+ # reply nobody asked for is one more signal in a world that already routes.
143
+ REPLY="${CLOSE_REPLY:-0}"
144
+
145
+ while [ $# -gt 0 ]; do
146
+ case "$1" in
147
+ --tid) TID="${2:-}"; shift 2 ;;
148
+ --workspace) WORKSPACE="${2:-one}"; shift 2 ;;
149
+ --status) STATUS="${2:-done}"; shift 2 ;;
150
+ --composite) COMPOSITE="${2:-}"; shift 2 ;;
151
+ --edge) EDGE="${2:-}"; shift 2 ;;
152
+ --dims) DIMS="${2:-}"; shift 2 ;;
153
+ --tags) TAGS="${2:-}"; shift 2 ;;
154
+ --only) ONLY="${2:-}"; shift 2 ;;
155
+ --budget) BUDGET="${2:-45}"; shift 2 ;;
156
+ --leg-budget) LEG_BUDGET="${2:-12}"; shift 2 ;;
157
+ --notify) NOTIFY=1; shift ;;
158
+ --no-file) NOFILE=1; shift ;;
159
+ --strict) STRICT=1; shift ;;
160
+ --json) JSON=1; shift ;;
161
+ --dry-run) DRY=1; shift ;;
162
+ --pending) PENDING=1; shift ;;
163
+ --self-test) SELFTEST=1; shift ;;
164
+ -h|--help) sed -n '/^# USAGE/,/^# END OF EVERY/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
165
+ --metrics) METRICS=1; shift ;;
166
+ --reply) REPLY=1; shift ;;
167
+ --no-reply) REPLY=0; shift ;;
168
+ --decide) DECIDE=1; shift ;;
169
+ --no-decide) DECIDE=0; shift ;;
170
+ --no-metrics) METRICS=0; shift ;;
171
+ --*) echo "do-close: unknown flag $1" >&2; exit 2 ;;
172
+ *) [ -z "$SLUG" ] && SLUG="$1" || { echo "do-close: unexpected arg $1" >&2; exit 2; }; shift ;;
173
+ esac
174
+ done
175
+
176
+ # ── credentials ─────────────────────────────────────────────────────────────
177
+ # Same ladder as do-signal.sh, same rule: the VALUE is never printed, only which
178
+ # file it came from (factory-repo.sh --check-env-indirection enforces this).
179
+ _env_key() { # <KEY>
180
+ local _f _v
181
+ for _f in "$ONE_ENV_FILE" "$ROOT/.env" $_MONOREPO_ENV_CANDIDATES; do
182
+ [ -f "$_f" ] || continue
183
+ _v=$(grep -E "^$1=" "$_f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//' || true)
184
+ [ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
185
+ done
186
+ printf ''
187
+ }
188
+
189
+ # ── the base URL, decided in <=1.5s and never guessed ───────────────────────
190
+ # Three doors disagreed on this for months: close.md said localhost:4321,
191
+ # do-signal.sh defaults to https://one.ie, do-promise-settle.sh to localhost.
192
+ # The ladder is now explicit and it is BOUNDED: an unset ONE_API_URL probes the
193
+ # local dev server for 1.5s and falls to prod. A dead local server costs 1.5s of
194
+ # a 45s budget, not a hang.
195
+ _resolve_base() {
196
+ if [ -n "${ONE_API_URL:-}" ]; then printf '%s' "$ONE_API_URL"; return; fi
197
+ if curl -s -o /dev/null --max-time 1.5 --connect-timeout 1 \
198
+ "http://localhost:4321/api/health" 2>/dev/null; then
199
+ printf 'http://localhost:4321'
200
+ else
201
+ printf 'https://one.ie'
202
+ fi
203
+ }
204
+
205
+ # ── bounded POST ────────────────────────────────────────────────────────────
206
+ # EVERY curl here carries --max-time AND --connect-timeout. do-signal.sh's
207
+ # _post_ask carries neither (do-signal.sh:89) — that is the hang this script
208
+ # exists to not inherit. Prints the body; exit 0 only on a 2xx that does not
209
+ # carry an `"error"`, because this estate answers HTTP 200 {error:...} when
210
+ # TypeDB is down and a closer must not read that as closed.
211
+ #
212
+ # THE THREE DOORS TAKE THREE DIFFERENT CREDENTIALS, and nothing said so until a
213
+ # close measured it (2026-09-09, all three on localhost:4321):
214
+ # /api/mark-dims Bearer GATEWAY_API_KEY — isVerifiedServiceCaller
215
+ # /api/ask/<receiver> Bearer GATEWAY_API_KEY — do-signal.sh's door
216
+ # /api/signal/<recv> Bearer SERVER_SECRET — signal-dispatch.ts:78-85
217
+ # Sending GATEWAY_API_KEY at the signal door returns `{"error":"unauthorized"}`
218
+ # with HTTP 401 — which is why /close's feedback pheromone, the step every
219
+ # future agent's routing reads, had never landed from this script's ancestor.
220
+ # The credential is chosen from the PATH, so a new door cannot inherit the wrong
221
+ # one by being added to the wrong helper.
222
+ _post() { # <url> <json> [max-time]
223
+ local _url="$1" _body="$2" _mt="${3:-$LEG_BUDGET}" _key _cfg _resp _code
224
+ case "$_url" in
225
+ */api/signal/*) _key="$(_env_key SERVER_SECRET)" ;;
226
+ *) _key="$(_env_key GATEWAY_API_KEY)" ;;
227
+ esac
228
+ _cfg="$(mktemp)"; chmod 600 "$_cfg"
229
+ [ -n "$_key" ] && printf 'header = "Authorization: Bearer %s"\n' "$_key" > "$_cfg"
230
+ _resp=$(curl -s -w '\n%{http_code}' --max-time "$_mt" --connect-timeout 2 \
231
+ -X POST "$_url" -H 'Content-Type: application/json' \
232
+ --config "$_cfg" -d "$_body" 2>/dev/null)
233
+ rm -f "$_cfg"
234
+ _code="${_resp##*$'\n'}"; _resp="${_resp%$'\n'*}"
235
+ printf '%s' "$_resp"
236
+ case "$_code" in
237
+ 2*) case "$_resp" in *'"error"'*) return 1 ;; *) return 0 ;; esac ;;
238
+ "") return 7 ;; # no answer at all — timed out or never connected
239
+ *) return 1 ;;
240
+ esac
241
+ }
242
+
243
+ # Escapes for a JSON string body. Newlines become a literal \n, not a space:
244
+ # `notes=` is multi-line on purpose (the detail, then the FIX command on its own
245
+ # line), and flattening it is what turns a paste-able command into prose.
246
+ _jsonesc() {
247
+ printf '%s' "${1:-}" \
248
+ | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' \
249
+ | awk 'BEGIN{ORS=""} NR>1{print "\\n"} {print}'
250
+ }
251
+
252
+ # ── bounded subprocesses ────────────────────────────────────────────────────
253
+ # Every network leg is bounded by curl's own --max-time. Two legs are not: they
254
+ # shell out to scripts this one does not control, and do-promise-settle.sh runs
255
+ # an arbitrary `proof:` command that can be anything at all. Those go through
256
+ # govern.sh's run_bounded — the repo's timeout(1) replacement, which kills the
257
+ # whole PROCESS GROUP (a plain `kill` reparents the children to launchd, which is
258
+ # how this repo leaked orphaned verify trees).
259
+ #
260
+ # THE TIMEOUT IS 143, NOT 124 — measured 2026-09-09:
261
+ # ( . lib/govern.sh; run_bounded 1 bash -c 'sleep 5' ); echo $? -> 143
262
+ # because run_bounded SIGTERMs the process group and bash reports 128+15. The
263
+ # first draft of this file branched on 124 (the GNU timeout(1) convention it
264
+ # assumed run_bounded kept) and those branches were dead: a `proof:` that ran out
265
+ # of clock fell through to the catch-all and was recorded **BROKEN** — a promise
266
+ # marked broken because the box was slow, which is the exact class the exit-3
267
+ # handling one branch over exists to prevent.
268
+ #
269
+ # 143 alone is not safe to key on either: a child can legitimately exit 143. So
270
+ # the timeout is read from run_bounded's OWN announcement on stderr, and only
271
+ # then normalised to 124 for the callers. A leg reads 124 as `unrun`, never as a
272
+ # verdict — a check that ran out of clock has told us nothing.
273
+ _bounded() { # <secs> <cmd...> — echoes stdout, returns the exit code (124 = timed out)
274
+ local _err _rc=0
275
+ _err="$(mktemp)"
276
+ if [ -r "$ROOT/.claude/scripts/lib/govern.sh" ] \
277
+ && ( . "$ROOT/.claude/scripts/lib/govern.sh" >/dev/null 2>&1; command -v run_bounded >/dev/null 2>&1 ); then
278
+ # shellcheck disable=SC1090
279
+ ( . "$ROOT/.claude/scripts/lib/govern.sh" >/dev/null 2>&1
280
+ run_bounded "$@" ) 2>"$_err" || _rc=$?
281
+ if grep -q '\[govern\] TIMEOUT' "$_err" 2>/dev/null; then _rc=124; fi
282
+ cat "$_err" >&2
283
+ else
284
+ shift
285
+ "$@" 2>"$_err" || _rc=$?
286
+ cat "$_err" >&2
287
+ fi
288
+ rm -f "$_err"
289
+ return "$_rc"
290
+ }
291
+
292
+ # ── the leg table ───────────────────────────────────────────────────────────
293
+ LEGS_ALL="gate derives task dims feedback promise"
294
+ _wanted() { # <leg> — honours --only
295
+ [ -z "$ONLY" ] && return 0
296
+ case ",$ONLY," in *",$1,"*) return 0 ;; *) return 1 ;; esac
297
+ }
298
+
299
+ RUNDIR="$(mktemp -d)"; trap 'rm -rf "$RUNDIR"' EXIT
300
+ _emit() { printf '%s\t%s\t%s\n' "$2" "$3" "${4:-}" > "$RUNDIR/$1"; } # <leg> <leg> <status> <detail>
301
+ _status_of() { cut -f2 "$RUNDIR/$1" 2>/dev/null; }
302
+ _detail_of() { cut -f3 "$RUNDIR/$1" 2>/dev/null; }
303
+
304
+ # The remediation table. `notes=` on a filed task is a PASTE-ABLE COMMAND, never
305
+ # prose — that is the whole difference between a task and a nag.
306
+ _fix_for() { # <leg>
307
+ local s="${SLUG:-<slug>}"
308
+ case "$1" in
309
+ gate) printf 'cd %s/one.ie/web && FULL_VERIFY=1 bun run verify # then: bash .claude/scripts/do-close.sh %s --only gate,task' "$ROOT" "$s" ;;
310
+ # gate,task — never `--only task` alone: the task leg needs the gate's verdict,
311
+ # and a fix command that skips the gate it is blocked on is not a fix.
312
+ task) printf 'bash .claude/scripts/do-close.sh %s --only gate,task%s' "$s" "${TID:+ --tid $TID}" ;;
313
+ dims) printf 'bash .claude/scripts/do-close.sh %s --only dims%s' "$s" "${DIMS:+ --dims $DIMS}" ;;
314
+ feedback) printf 'bash .claude/scripts/do-close.sh %s --only feedback' "$s" ;;
315
+ derives) printf 'bash .claude/scripts/do-derives-check.sh %s # write the artifact it names, then: bash .claude/scripts/do-close.sh %s --only derives,promise' "$s" "$s" ;;
316
+ promise) printf 'bash .claude/scripts/do-promise-settle.sh %s%s # then: bash .claude/scripts/do-close.sh %s --only promise' "$s" "${COMPOSITE:+ --composite $COMPOSITE}" "$s" ;;
317
+ *) printf 'bash .claude/scripts/do-close.sh %s --only %s' "$s" "$1" ;;
318
+ esac
319
+ }
320
+
321
+ # ═══ LEGS ═══════════════════════════════════════════════════════════════════
322
+
323
+ # gate — is there a FULL-suite receipt for this exact tree? Never runs a suite.
324
+ #
325
+ # test-full.sh is THE one definition of the full suite and /close is named in its
326
+ # header as one of its two callers — so the key this reads is byte-identical to
327
+ # the one deploy.sh mints, by construction. TEST_CACHE_KEY_ONLY=1 makes every
328
+ # test-cached.sh in its fan-out print its key and exit 0 without starting vitest:
329
+ # ~1s, and it CANNOT accidentally launch a suite. Reading the stamp back is
330
+ # release.sh's `_stamp_for`, same match-on-the-key rule (test-cached mangles the
331
+ # whole path into the filename, so matching the sha256 is the stable form).
332
+ leg_gate() {
333
+ if [ "$STATUS" != "done" ]; then _emit gate gate n/a "status=$STATUS — a non-done close asserts no green"; return; fi
334
+ if [ ! -f "$ROOT/.claude/scripts/test-full.sh" ]; then _emit gate gate n/a "no test-full.sh in this tree"; return; fi
335
+ local keys k n=0 hit=0 stamp="" rc=0
336
+ keys=$(cd "$ROOT" && TEST_CACHE_KEY_ONLY=1 _bounded "$LEG_BUDGET" bash .claude/scripts/test-full.sh 2>/dev/null | grep -Eo '^[0-9a-f]{64}$') || rc=$?
337
+ if [ -z "$keys" ]; then _emit gate gate unrun "test-full.sh printed no receipt key (rc=$rc)"; return; fi
338
+ for k in $keys; do
339
+ n=$((n+1))
340
+ for f in "$CACHE_DIR"/*"$k".pass; do
341
+ [ -f "$f" ] && { hit=$((hit+1)); [ -z "$stamp" ] && stamp="$(cat "$f" 2>/dev/null)"; break; }
342
+ done
343
+ done
344
+ if [ "$hit" -eq "$n" ]; then
345
+ _emit gate gate pass "full-suite receipt on this exact tree — $hit/$n lane(s), minted $stamp"
346
+ else
347
+ # NOT a fast pass wearing a full badge. The close does not mint a receipt and
348
+ # does not pretend one exists; it names the command that mints one.
349
+ _emit gate gate unrun "no full-suite receipt — $hit/$n lane(s) stamped for this tree; the close does not mint one"
350
+ fi
351
+ }
352
+
353
+ # derives — every `derives: <key>: true` file-shaped artifact present on disk.
354
+ leg_derives() {
355
+ if [ -z "$SLUG" ] || [ ! -f "$ROOT/text/$SLUG.md" ]; then _emit derives derives n/a "no text/$SLUG.md promise"; return; fi
356
+ local out rc=0
357
+ out=$(cd "$ROOT" && _bounded "$LEG_BUDGET" bash .claude/scripts/do-derives-check.sh "$SLUG" 2>&1) || rc=$?
358
+ case "$rc" in
359
+ 0) _emit derives derives pass "all declared artifacts on disk" ;;
360
+ 124) _emit derives derives unrun "do-derives-check.sh exceeded ${LEG_BUDGET}s" ;;
361
+ *) _emit derives derives fail "exit=$rc $(printf '%s' "$out" | tail -2 | tr '\n' ' ')" ;;
362
+ esac
363
+ }
364
+
365
+ # task — the board write. GATED on `gate` for status=done (see the header).
366
+ leg_task() {
367
+ if [ -z "$TID" ] && [ -z "$SLUG" ]; then _emit task task n/a "no --tid and no slug — nothing addressable"; return; fi
368
+ if [ "$STATUS" = "timeout" ]; then _emit task task n/a "timeout leaves the lease held — status untouched, by rule"; return; fi
369
+ # ONLY pass (a receipt exists) or n/a (no receipt is claimed — a non-done close,
370
+ # or a tree with no suite) may license the board write. `skipped` may not: an
371
+ # operator narrowing --only is not evidence about the tree.
372
+ local g; g="$(_status_of gate)"
373
+ if [ "$STATUS" = "done" ] && [ "$g" != "pass" ] && [ "$g" != "n/a" ]; then
374
+ _emit task task unrun "gate=$g — refusing to write done over an unproven tree"
375
+ return
376
+ fi
377
+ if [ -z "$TID" ]; then
378
+ # The slug door: resolves by the `slug:` tag and stamps provenance as it closes.
379
+ local out rc=0
380
+ out=$(cd "$ROOT" && bash .claude/scripts/do-signal.sh --task-link "$SLUG" "docs=$SLUG" 2>&1) || rc=$?
381
+ if [ "$rc" -eq 0 ]; then _emit task task pass "closed via slug door ($SLUG)"
382
+ else _emit task task unrun "slug door exit=$rc $(printf '%s' "$out" | tail -1)"; fi
383
+ return
384
+ fi
385
+ local body resp rc=0
386
+ body="{\"data\":{\"tid\":\"$(_jsonesc "$TID")\",\"status\":\"$STATUS\",\"workspace\":\"$(_jsonesc "$WORKSPACE")\"}}"
387
+ resp=$(_post "$BASE/api/ask/tasks:status" "$body") || rc=$?
388
+ case "$rc" in
389
+ 0) _emit task task pass "tid=$TID status=$STATUS" ;;
390
+ 7) _emit task task unrun "tasks:status door did not answer inside ${LEG_BUDGET}s" ;;
391
+ *) _emit task task fail "tasks:status refused: $(printf '%s' "$resp" | head -c 160)" ;;
392
+ esac
393
+ }
394
+
395
+ # dims — the five rubric axes onto the path. Bare {edge,dims}: mark-dims.ts:11
396
+ # Zod-parses exactly that; a `{data:{...}}` wrapper is a 400.
397
+ leg_dims() {
398
+ if [ -z "$DIMS" ]; then _emit dims dims "n/a" "no --dims supplied"; return; fi
399
+ if [ "$STATUS" = "timeout" ]; then _emit dims dims n/a "timeout is neutral — no mark, no warn"; return; fi
400
+ local d body resp rc=0
401
+ d=$(printf '%s' "$DIMS" | awk -F',' '{for(i=1;i<=NF;i++){split($i,a,"=");printf "%s\"%s\":%s",(i>1?",":""),a[1],a[2]}}')
402
+ body="{\"edge\":\"$(_jsonesc "$CLOSE_EDGE")\",\"dims\":{$d}}"
403
+ resp=$(_post "$BASE/api/mark-dims" "$body") || rc=$?
404
+ case "$rc" in
405
+ 0) _emit dims dims pass "edge=$CLOSE_EDGE dims=$DIMS" ;;
406
+ 7) _emit dims dims unrun "mark-dims door did not answer inside ${LEG_BUDGET}s" ;;
407
+ *) _emit dims dims fail "mark-dims refused: $(printf '%s' "$resp" | head -c 160)" ;;
408
+ esac
409
+ }
410
+
411
+ # feedback — the return-path pheromone. The receiver is a PATH SEGMENT, never a
412
+ # body field: /api/signal with no segment is 400 `receiver required`.
413
+ leg_feedback() {
414
+ local tags outcome body resp rc=0
415
+ case "$STATUS" in
416
+ done) outcome=result ;; failed) outcome=failure ;;
417
+ dissolved) outcome=dissolved ;; timeout) outcome=timeout ;;
418
+ *) outcome="$STATUS" ;;
419
+ esac
420
+ # THE TAGS ARE DERIVED, NOT DECLARED — see § the tags this close earned. They
421
+ # are what the world routes on, so a close that measured `degraded` and `slow`
422
+ # reaches whoever staked on those words, not just whoever watches `close`.
423
+ tags=$(printf '%s' "${CLOSE_TAGS:-close ${SLUG:-session}}" | awk '{for(i=1;i<=NF;i++){printf "%s\"%s\"",(i>1?",":""),$i}}')
424
+ # AS MUCH DATA AS THE CLOSE HAS. The return path is the one channel the next
425
+ # agent's routing actually reads; it used to carry four fields and so said only
426
+ # that a close happened. It now carries every leg's state AND its seconds, the
427
+ # decisions taken and recommended with their confidence, the verdict, the wall
428
+ # clock, and the whole measured block — one instant, measured once, above.
429
+ local metrics='null'
430
+ [ -f "$METRICS_JSON" ] && [ -s "$METRICS_JSON" ] && metrics="$(cat "$METRICS_JSON")"
431
+ body="{\"data\":{\"tags\":[$tags],\"strength\":\"${COMPOSITE:-0}\",\"content\":{\"slug\":\"$(_jsonesc "${SLUG:-}")\",\"task_id\":\"$(_jsonesc "$TID")\",\"composite\":${COMPOSITE:-0},\"outcome\":\"$outcome\",\"closed\":${CLOSED:-false},\"pass\":${PASS:-0},\"fail\":${FAIL:-0},\"unrun\":${UNRUN:-0},\"na\":${NA:-0},\"elapsed_s\":$(( $(date +%s) - T0 )),\"door\":\"$BASE\",\"branch\":\"$(git -C "$ROOT" branch --show-current 2>/dev/null || echo '')\",\"legs\":[${CLOSE_LEGS_JSON:-}],\"decisions\":[${CLOSE_DECIDE_JSON:-}],\"metrics\":$metrics,\"hop\":${ACCRETE_HOP:-1},\"accretion\":{\"budget_bytes\":${ACCRETE_BUDGET:-4096},\"rule\":\"base<<hop, capped ${ACCRETE_MAX:-262144}; bulk by reference\"},\"artifacts\":[${ARTIFACTS:-}],\"diffstat\":\"$(_jsonesc "${DIFFSTAT:-}")\",\"origin\":\"$(_jsonesc "$ACCRETE_ORIGIN")\",\"sender\":\"$(_jsonesc "$ACCRETE_SENDER")\",\"reply_to\":\"$(_jsonesc "$ACCRETE_REPLY")\",\"trail\":[${TRAIL:-}]}}}"
432
+ FEEDBACK_BODY="$body" # kept for the return leg below — the same envelope, verbatim
433
+ resp=$(_post "$BASE/api/signal/loop:feedback" "$body") || rc=$?
434
+ case "$rc" in
435
+ 0) _emit feedback feedback pass "outcome=$outcome tags=$CLOSE_TAGS hop=$ACCRETE_HOP" ;;
436
+ 7) _emit feedback feedback unrun "signal door did not answer inside ${LEG_BUDGET}s" ;;
437
+ *) _emit feedback feedback fail "signal refused: $(printf '%s' "$resp" | head -c 160)" ;;
438
+ esac
439
+ }
440
+
441
+ # promise — settle off-chain. EXIT 3 IS NOT A CLOSE. A `proof:` is an &&-join, so
442
+ # a leg that cannot reach its substrate returns 3 = UNVERIFIABLE. Folding that
443
+ # into `warn` deposits resistance on a proven path for a cluster blip — a promise
444
+ # recorded BROKEN for a reason its maker cannot fix (text/factory.md § "red" vs
445
+ # "cannot run"). Here it is `unrun`: the promise stays open and gets a row.
446
+ leg_promise() {
447
+ # A halt is not a settlement. --status timeout means the work is UNFINISHED and
448
+ # the lease is still held; running the proof there records BROKEN on a promise
449
+ # nobody has finished making. Same rule as task and dims: timeout writes nothing.
450
+ if [ "$STATUS" = "timeout" ]; then _emit promise promise n/a "timeout — the work is unfinished; a proof run now measures nothing"; return; fi
451
+ if [ -z "$SLUG" ] || [ ! -f "$ROOT/text/$SLUG.md" ]; then _emit promise promise n/a "no text/$SLUG.md"; return; fi
452
+ if ! grep -qE '^\s*proof:' "$ROOT/text/$SLUG.md" 2>/dev/null; then
453
+ _emit promise promise n/a "promise carries no proof: — nothing observable to settle"; return; fi
454
+ local dv; dv="$(_status_of derives)"
455
+ if [ "$dv" = "fail" ]; then _emit promise promise unrun "derives=fail — a missing artifact forces BROKEN; fix the artifact first"; return; fi
456
+ local out rc=0
457
+ out=$(cd "$ROOT" && _bounded "$LEG_BUDGET" bash .claude/scripts/do-promise-settle.sh "$SLUG" ${COMPOSITE:+--composite "$COMPOSITE"} 2>&1) || rc=$?
458
+ case "$rc" in
459
+ 0) _emit promise promise pass "KEPT — mark on promise:${SLUG}→proof" ;;
460
+ 3) _emit promise promise unrun "UNVERIFIABLE (exit 3) — proof could not reach its substrate; neither verb written, promise stays open" ;;
461
+ 124) _emit promise promise unrun "proof exceeded ${LEG_BUDGET}s — a proof out of clock is not a broken promise" ;;
462
+ *) _emit promise promise fail "BROKEN (exit $rc) — warn on promise:${SLUG}→proof" ;;
463
+ esac
464
+ }
465
+
466
+ # ═══ escalation ═════════════════════════════════════════════════════════════
467
+ # Every non-pass leg lands in the LOCAL ledger first — that write cannot fail and
468
+ # it is what `--pending` reads back. Then, best-effort and bounded, a board task
469
+ # whose notes are the fix command. Only when the board write ALSO fails does a
470
+ # human get pinged: a ledger nothing reads and a board nobody reached means the
471
+ # only remaining channel is the person.
472
+ _ledger_row() { # <leg> <status> <detail> <filed> [decide]
473
+ local k row
474
+ k="${SLUG:-session}:$1"
475
+ row=$(printf '{"k":"%s","at":"%s","slug":"%s","leg":"%s","status":"%s","detail":"%s","fix":"%s","tid":"%s","workspace":"%s","filed":"%s","decide":"%s"}' \
476
+ "$(_jsonesc "$k")" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(_jsonesc "${SLUG:-}")" "$1" "$2" \
477
+ "$(_jsonesc "$3")" "$(_jsonesc "$(_fix_for "$1")")" "$(_jsonesc "$TID")" "$(_jsonesc "$WORKSPACE")" "$4" "${5:-}")
478
+ mkdir -p "$(dirname "$LEDGER")" 2>/dev/null
479
+ # Keyed union: a re-run REPLACES its own row rather than stacking duplicates,
480
+ # so the ledger measures open work, not close attempts (deploy-record.sh's
481
+ # _union, one file shape simpler).
482
+ if [ -f "$LEDGER" ]; then
483
+ grep -v "\"k\":\"$(_jsonesc "$k")\"" "$LEDGER" 2>/dev/null > "$LEDGER.tmp" || true
484
+ mv "$LEDGER.tmp" "$LEDGER"
485
+ fi
486
+ printf '%s\n' "$row" >> "$LEDGER"
487
+ }
488
+ _ledger_clear() { # <leg> — a leg that passed owes nothing
489
+ local k="${SLUG:-session}:$1"
490
+ [ -f "$LEDGER" ] || return 0
491
+ grep -v "\"k\":\"$(_jsonesc "$k")\"" "$LEDGER" 2>/dev/null > "$LEDGER.tmp" || true
492
+ mv "$LEDGER.tmp" "$LEDGER"
493
+ }
494
+
495
+ _file_task() { # <leg> <status> <detail> → 0 filed, 1 not
496
+ [ "$NOFILE" -eq 1 ] && return 1
497
+ local title fix out rc=0
498
+ title="close ${SLUG:-session}: $1 is $2"
499
+ fix="$(_fix_for "$1")"
500
+ out=$(cd "$ROOT" && bash .claude/scripts/do-signal.sh --task-create \
501
+ "close-${SLUG:-session}-$1" "$title" \
502
+ "tags=close,unclosed,leg:$1" \
503
+ "notes=$3
504
+
505
+ FIX (paste this):
506
+ $fix" 2>&1) || rc=$?
507
+ [ "$rc" -eq 0 ]
508
+ }
509
+
510
+ _notify_human() { # <summary>
511
+ [ -x "$ROOT/.claude/scripts/notify.sh" ] || return 1
512
+ bash "$ROOT/.claude/scripts/notify.sh" "$1" >/dev/null 2>&1
513
+ }
514
+
515
+ # ═══ --pending ══════════════════════════════════════════════════════════════
516
+ # Two states, and it says WHICH — printing the wrong instruction on every run is
517
+ # how a nudge stops being read. Silent and 0 when clean.
518
+ if [ "$PENDING" -eq 1 ]; then
519
+ [ -f "$LEDGER" ] || exit 0
520
+ n=$(grep -c . "$LEDGER" 2>/dev/null || echo 0)
521
+ [ "$n" -eq 0 ] && exit 0
522
+ echo "do-close: $n leg(s) never closed —"
523
+ if command -v jq >/dev/null 2>&1; then
524
+ jq -r '" \(.k)\n \(.status) \(.detail)\n fix: \(.fix)"' < "$LEDGER"
525
+ else
526
+ # sed on a JSON string truncates at the first escaped quote — a detail like
527
+ # {\"error\":\"unauthorized\"} printed as `{\`. Without jq, print the raw row
528
+ # rather than a mangled one: a half-shown fix command is worse than none.
529
+ cat "$LEDGER"
530
+ fi
531
+ exit 6
532
+ fi
533
+
534
+ # ═══ --self-test ════════════════════════════════════════════════════════════
535
+ if [ "$SELFTEST" -eq 1 ]; then
536
+ fails=0
537
+ echo "── do-close.sh --self-test ──"
538
+ # Every sub-run gets a SANDBOX ledger and a SANDBOX receipt cache. A self-test
539
+ # that files rows into the operator's real close ledger is a self-test that
540
+ # makes `--pending` lie on the next real run.
541
+ _sbl="$(mktemp -d)/ledger.jsonl"
542
+ export CLOSE_LEDGER="$_sbl"
543
+
544
+ # 1. GREEN: a dry run reports every leg and never touches a door.
545
+ out=$(bash "$0" __probe__ --dry-run 2>&1)
546
+ if printf '%s' "$out" | grep -q 'DRY-RUN'; then echo "ok: --dry-run plans without POSTing"
547
+ else echo "FAIL: --dry-run did not announce itself"; fails=$((fails+1)); fi
548
+
549
+ # 2. RED PROOF — an unrun leg must never read as closed. Force the gate unrun
550
+ # by pointing the receipt cache at an empty dir, and assert closed=false.
551
+ out=$(TEST_CACHE_DIR="$(mktemp -d)" CLOSE_NO_ESCALATE=1 bash "$0" __probe__ --only gate --no-file 2>&1)
552
+ if printf '%s' "$out" | grep -q 'gate .*unrun'; then
553
+ if printf '%s' "$out" | grep -q 'closed=false'; then
554
+ echo "ok: RED PROOF — a missing full-suite receipt is unrun and the close is NOT closed"
555
+ else echo "FAIL: RED PROOF — unrun gate still reported closed=true"; fails=$((fails+1)); fi
556
+ else echo "FAIL: RED PROOF — an empty receipt cache did not surface as an unrun gate"; fails=$((fails+1)); fi
557
+
558
+ # 3. RED PROOF — the dependency edge. An unrun gate must BLOCK the done write.
559
+ out=$(TEST_CACHE_DIR="$(mktemp -d)" CLOSE_NO_ESCALATE=1 bash "$0" __probe__ --tid t_probe --only gate,task --no-file 2>&1)
560
+ if printf '%s' "$out" | grep -q 'task .*unrun.*refusing to write done'; then
561
+ echo "ok: RED PROOF — no receipt ⇒ the board is NOT told done"
562
+ else echo "FAIL: RED PROOF — the task leg wrote done over an unproven tree"; fails=$((fails+1)); fi
563
+
564
+ # 4. GREEN PROOF — the edge is not stuck red: a gate that is n/a lets task run.
565
+ out=$(CLOSE_NO_ESCALATE=1 bash "$0" __probe__ --tid t_probe --status failed --only gate,task --no-file --dry-run 2>&1)
566
+ if printf '%s' "$out" | grep -q 'gate .*n/a'; then
567
+ echo "ok: GREEN PROOF — a non-done close does not demand a green receipt"
568
+ else echo "FAIL: GREEN PROOF — status=failed still demanded a receipt"; fails=$((fails+1)); fi
569
+
570
+ # 5. The ledger round-trips: a filed leg comes back out of --pending with its fix.
571
+ tmpl="$(mktemp)"; rm -f "$tmpl"
572
+ out=$(LEDGER_OVERRIDE=1 CLOSE_LEDGER="$tmpl" TEST_CACHE_DIR="$(mktemp -d)" CLOSE_NO_ESCALATE=1 \
573
+ bash "$0" __probe__ --only gate --no-file 2>&1)
574
+ out2=$(CLOSE_LEDGER="$tmpl" bash "$0" --pending 2>&1); rc=$?
575
+ if [ "$rc" -eq 6 ] && printf '%s' "$out2" | grep -q 'fix: '; then
576
+ echo "ok: an unclosed leg round-trips through --pending WITH its fix command (exit 6)"
577
+ else echo "FAIL: --pending did not return the unclosed leg (rc=$rc)"; fails=$((fails+1)); fi
578
+
579
+ # 6. RED PROOF for the ledger itself — gut the writer and assert check 5 breaks.
580
+ gut="$(mktemp)"; awk '/^_ledger_row\(\) \{/{print "_ledger_row() { :; }"; sk=1; next} sk&&/^\}$/{sk=0;next} !sk' "$0" > "$gut"
581
+ tmpl2="$(mktemp)"; rm -f "$tmpl2"
582
+ CLOSE_LEDGER="$tmpl2" TEST_CACHE_DIR="$(mktemp -d)" CLOSE_NO_ESCALATE=1 bash "$gut" __probe__ --only gate --no-file >/dev/null 2>&1
583
+ CLOSE_LEDGER="$tmpl2" bash "$0" --pending >/dev/null 2>&1; rc=$?
584
+ if [ "$rc" -eq 0 ]; then echo "ok: RED PROOF — a gutted ledger writer makes --pending go blind (check 5 measures something)"
585
+ else echo "FAIL: RED PROOF — --pending still found a row after the writer was gutted"; fails=$((fails+1)); fi
586
+ rm -f "$gut" "$tmpl" "$tmpl2"
587
+
588
+ # 7. RED PROOF — `--only task` must NOT become a door around the receipt gate.
589
+ # This is the defect the first draft shipped: a skipped gate reported n/a,
590
+ # leg_task read n/a as licence, and the fix string it advertised for
591
+ # "refusing to write done over an unproven tree" was `--only task` — the
592
+ # command that wrote done over an unproven tree.
593
+ out=$(TEST_CACHE_DIR="$(mktemp -d)" CLOSE_NO_ESCALATE=1 bash "$0" __probe__ --tid t_probe --only task --no-file 2>&1)
594
+ if printf '%s' "$out" | grep -q 'task .*unrun'; then
595
+ echo "ok: RED PROOF — --only task cannot bypass the receipt gate"
596
+ else echo "FAIL: RED PROOF — --only task wrote the board with no gate verdict"; fails=$((fails+1)); fi
597
+
598
+ # 8. And the fix it advertises must be the one that works: gate,task.
599
+ tf=$(bash -c "ROOT='$ROOT'; SLUG=x; TID=''; DIMS=''; COMPOSITE=''; $(declare -f _fix_for); _fix_for task" 2>/dev/null)
600
+ case "$tf" in
601
+ *"--only gate,task"*) echo "ok: the task leg's fix command runs the gate it is blocked on" ;;
602
+ *) echo "FAIL: the task fix skips its own gate: $tf"; fails=$((fails+1)) ;;
603
+ esac
604
+
605
+ # 9. RED PROOF — the bound reports a TIMEOUT as unrun, never as a verdict.
606
+ # run_bounded exits 143 (SIGTERM), not 124; branching on 124 alone made
607
+ # every timed-out proof record BROKEN.
608
+ tb=$(bash -c "ROOT='$ROOT'; $(declare -f _bounded); _bounded 1 bash -c 'sleep 4'; echo rc=\$?" 2>/dev/null | tail -1)
609
+ if [ "$tb" = "rc=124" ]; then echo "ok: RED PROOF — a bounded call that times out returns 124, so a slow proof is unrun, not BROKEN"
610
+ else echo "FAIL: a timed-out bounded call returned '$tb', not rc=124 — a slow proof would record BROKEN"; fails=$((fails+1)); fi
611
+
612
+ # 10. Every leg's remediation command NAMES that leg (a fix that merely
613
+ # contains the word do-close.sh is a tautology, not a check).
614
+ miss=0
615
+ for l in $LEGS_ALL; do
616
+ f=$(bash -c "ROOT='$ROOT'; SLUG=x; TID=''; DIMS=''; COMPOSITE=''; $(declare -f _fix_for); _fix_for $l" 2>/dev/null)
617
+ case "$f" in *"$l"*) ;; *) echo "FAIL: leg $l fix does not name the leg: $f"; miss=1 ;; esac
618
+ done
619
+ [ "$miss" -eq 0 ] && echo "ok: every leg carries a paste-able fix command that names it"
620
+
621
+ # 11. The decide wiring: a `highway` verdict is TAKEN, reported with its undo,
622
+ # and leaves a `taken:` ledger row so the NEXT close can settle it. Driven
623
+ # through a gutted copy of the real do-decide.sh, never through seeded
624
+ # evidence — five fake marks would make the test its own oracle.
625
+ _dg="$(mktemp)"
626
+ awk '/^_verdict\(\) \{/{print; print " printf \"highway mint\"; return 0;"; next} {print}' \
627
+ "$ROOT/.claude/scripts/do-decide.sh" > "$_dg"; chmod +x "$_dg"
628
+ tmpd="$(mktemp)"
629
+ out=$(CLOSE_LEDGER="$tmpd" TEST_CACHE_DIR="$(mktemp -d)" DECIDE_BIN="$_dg" DECIDE_DRYCMD=1 \
630
+ CLOSE_NO_ESCALATE=1 bash "$0" __probe__ --only gate --no-file --no-metrics 2>&1)
631
+ if printf '%s' "$out" | grep -q 'TAKEN mint' && printf '%s' "$out" | grep -q 'undo:' \
632
+ && grep -q '"decide":"taken:mint"' "$tmpd" 2>/dev/null; then
633
+ echo "ok: a highway verdict is TAKEN, carries its undo, and leaves a settle-able row"
634
+ else echo "FAIL: the decide wiring did not take a highway verdict: $out"; fails=$((fails+1)); fi
635
+
636
+ # 12. RED PROOF for 11 — --no-decide must leave the same fork entirely alone.
637
+ tmpd2="$(mktemp)"
638
+ out=$(CLOSE_LEDGER="$tmpd2" TEST_CACHE_DIR="$(mktemp -d)" DECIDE_BIN="$_dg" DECIDE_DRYCMD=1 \
639
+ CLOSE_NO_ESCALATE=1 bash "$0" __probe__ --only gate --no-file --no-metrics --no-decide 2>&1)
640
+ if ! printf '%s' "$out" | grep -q 'TAKEN'; then
641
+ echo "ok: RED PROOF — --no-decide decides nothing on the same forced highway (check 11 measures something)"
642
+ else echo "FAIL: RED PROOF — --no-decide still took a decision"; fails=$((fails+1)); fi
643
+ rm -f "$_dg" "$tmpd" "$tmpd2"
644
+
645
+ # 13. A tag can never carry a `/`. land.sh and deploy.sh pass a BRANCH NAME as
646
+ # the slug, and the mark edge is a URL path segment — `feat/x` split the
647
+ # route and the verb vanished with no error. Measured: 6 tags, 5 verbs.
648
+ _tt=$( CLOSE_TAGS=""
649
+ eval "$(sed -n '/^_tag_add() {/,/^}/p' "$0")"
650
+ _tag_add "feat/test-branch"; _tag_add "a b"; _tag_add "ok-1"
651
+ printf '%s' "$CLOSE_TAGS" )
652
+ case "$_tt" in
653
+ */*|*" a b "*) echo "FAIL: _tag_add let an unroutable tag through: $_tt"; fails=$((fails+1)) ;;
654
+ *) echo "ok: a branch-shaped slug is sanitised to a routable tag ($_tt)" ;;
655
+ esac
656
+ # 14. The promise leg's TERMINAL branches must survive `set -u`.
657
+ #
658
+ # Both were dead from this file's first commit (b6641e812, 2026-09-09):
659
+ # `"KEPT — mark on promise:$SLUG→proof"` runs the U+2192 bytes straight
660
+ # into the variable, bash does not end the name there, and under `set -u`
661
+ # the whole function aborts. 459 was the `pass` case and 462 the `fail`
662
+ # case, so exit 3 and 124 — the two UNRUN branches — were the only
663
+ # outcomes that ever reached a reader. The leg is usually unrun, and an
664
+ # unrun leg prints a reason and moves on, which is why four days of
665
+ # closes went by without anyone seeing it.
666
+ #
667
+ # Checks 1-13 could not catch this: not one of them drives the promise
668
+ # leg to a verdict. That hole is the reason the defect existed, so this
669
+ # check evaluates the real emit strings from the real file rather than a
670
+ # copy of them — a hand-written fixture would pass while the script
671
+ # stayed broken.
672
+ _pe=$(
673
+ set -u
674
+ SLUG="probe"; rc=7
675
+ for _ln in 459 462; do
676
+ _src=$(sed -n "${_ln}p" "$0" | sed 's/^ *[0-9*)]*) *_emit promise promise [a-z]* //; s/ *;;$//')
677
+ eval "printf '%s\n' $_src" 2>&1 || printf 'DIED\n'
678
+ done
679
+ )
680
+ case "$_pe" in
681
+ *"unbound variable"*|*DIED*)
682
+ echo "FAIL: a promise leg terminal branch dies under set -u — it can never report a verdict"; fails=$((fails+1)) ;;
683
+ *KEPT*probe*|*BROKEN*probe*)
684
+ echo "ok: the promise leg's pass and fail branches both survive set -u and name the slug" ;;
685
+ *)
686
+ echo "FAIL: could not drive the promise leg's terminal branches (got: ${_pe:-empty})"; fails=$((fails+1)) ;;
687
+ esac
688
+
689
+ # 14b. RED PROOF for 14 — the unbraced form must STILL die, or 14 proves nothing.
690
+ #
691
+ # First attempt failed honestly and is worth recording: it passed the
692
+ # escape "\u2192" through eval, where bash printf leaves it literal, so
693
+ # the arrow bytes never sat against the variable and the form could not
694
+ # fail. The check correctly reported "the unbraced form no longer fails".
695
+ # A red proof that cannot go red is the thing this check exists to catch,
696
+ # and it caught itself. The character is written literally now.
697
+ _rp=$(bash -uc 'SLUG=probe; printf "%s\n" "KEPT on promise:$SLUG→proof"' 2>&1 || printf 'DIED\n')
698
+ case "$_rp" in
699
+ *"unbound variable"*|*DIED*) echo "ok: RED PROOF — the unbraced form still dies, so check 14 measures something" ;;
700
+ *) echo "FAIL: RED PROOF — the unbraced form no longer fails; check 14 proves nothing"; fails=$((fails+1)) ;;
701
+ esac
702
+
703
+ fails=$((fails+miss))
704
+
705
+ echo "──"
706
+ [ "$fails" -eq 0 ] && { echo "do-close self-test: PASS"; exit 0; }
707
+ echo "do-close self-test: $fails FAILURE(S)"; exit 1
708
+ fi
709
+
710
+ # ═══ RUN ════════════════════════════════════════════════════════════════════
711
+ CLOSE_EDGE="${EDGE:-close:${SLUG:-session}→result}"
712
+ T0=$(date +%s)
713
+ BASE="$( [ "$DRY" -eq 1 ] && printf 'http://dry.invalid' || _resolve_base )"
714
+
715
+ _run_leg() { # <leg> — one leg, bounded, always emits
716
+ local leg="$1"
717
+ # `skipped`, never `n/a`. n/a means "this leg does not apply, by rule" and it is
718
+ # what licenses the dependent legs to proceed. A leg the operator merely left
719
+ # out of --only has proved nothing, and must license nothing.
720
+ if ! _wanted "$leg"; then _emit "$leg" "$leg" skipped "not in --only"; return; fi
721
+ if [ "$DRY" -eq 1 ]; then
722
+ # A dry run still evaluates the CHEAP local preconditions, so the plan it
723
+ # prints is the plan that would run — not a list of leg names.
724
+ case "$leg" in
725
+ gate|derives|promise) "leg_$leg" ;;
726
+ *) _emit "$leg" "$leg" unrun "DRY-RUN — would POST $BASE" ;;
727
+ esac
728
+ return
729
+ fi
730
+ # THE BUDGET DOES NOT STARVE THE CLOSING SIGNAL. Every other leg is skipped
731
+ # once the wall clock is spent — that is what bounds this script. `feedback` is
732
+ # exempt because it is wave 3: it runs after the ~17s measured block, so on any
733
+ # slow box the budget check would fire on the one leg that CARRIES the whole
734
+ # close, and Rule 1 (every signal closes) would be broken by its own guard. It
735
+ # is still bounded — by its own --leg-budget, like every curl here.
736
+ local elapsed=$(( $(date +%s) - T0 ))
737
+ if [ "$elapsed" -ge "$BUDGET" ] && [ "$leg" != "feedback" ]; then
738
+ _emit "$leg" "$leg" unrun "budget ${BUDGET}s spent before this leg started"; return; fi
739
+ # SPEED IS A MEASURED ROW, NOT A FEELING. Each leg's own wall clock is written
740
+ # beside its verdict, because "the close was slow" and "the promise proof was
741
+ # slow" are different facts with different fixes — and because the signal below
742
+ # carries these numbers, so the next close can see which door is drifting.
743
+ local _t0; _t0=$(date +%s)
744
+ "leg_$leg"
745
+ printf '%s' "$(( $(date +%s) - _t0 ))" > "$RUNDIR/$leg.s" 2>/dev/null
746
+ [ -f "$RUNDIR/$leg" ] || _emit "$leg" "$leg" unrun "leg produced no verdict"
747
+ }
748
+ _secs_of() { cat "$RUNDIR/$1.s" 2>/dev/null || printf '0'; }
749
+
750
+ # Two waves, because there are exactly two dependency edges and no more:
751
+ # task ← gate (never write done over an unproven tree)
752
+ # promise ← derives (a missing artifact forces BROKEN — fix it, don't record it)
753
+ # Everything else is independent and goes in parallel.
754
+ for l in gate derives dims; do _run_leg "$l" & done
755
+ wait
756
+ for l in task promise; do _run_leg "$l" & done
757
+ wait
758
+ # `feedback` is the THIRD wave and it is the only leg that runs after the
759
+ # verdict — a third dependency edge the two-wave design did not have:
760
+ # feedback ← everything else
761
+ # It used to fire in wave 1 carrying four fields (slug, tid, composite, outcome),
762
+ # which is a pheromone that says a close happened and nothing about what it found.
763
+ # The return path is the one channel the next agent's routing actually reads, so
764
+ # it now carries the whole close: every leg's state AND its seconds, the decisions
765
+ # taken and recommended with their confidence, the derived tags, and the measured
766
+ # block. The cost of moving it is one ordering constraint; the cost of not moving
767
+ # it was a signal nobody could learn from.
768
+
769
+ # ── verdict ─────────────────────────────────────────────────────────────────
770
+ PASS=0; FAIL=0; UNRUN=0; NA=0; OPEN_LEGS=""
771
+ # The five that have run. `feedback` is folded in below, after it fires with the
772
+ # verdict in its payload — it reports ON this verdict, so it cannot be inside it.
773
+ _fold_leg() { # <leg>
774
+ local l="$1" s
775
+ s="$(_status_of "$l")"; [ -n "$s" ] || s=unrun
776
+ case "$s" in
777
+ pass) PASS=$((PASS+1)); [ "$DRY" -eq 1 ] || _ledger_clear "$l" ;;
778
+ fail) FAIL=$((FAIL+1)); OPEN_LEGS="$OPEN_LEGS $l" ;;
779
+ unrun) UNRUN=$((UNRUN+1)); OPEN_LEGS="$OPEN_LEGS $l" ;;
780
+ n/a) NA=$((NA+1)) ;;
781
+ # skipped: the operator narrowed --only. Not a pass, not a debt — this run
782
+ # made no claim about it, so it neither blocks `closed` nor files a row.
783
+ skipped) ;;
784
+ esac
785
+ }
786
+ for l in gate derives task dims promise; do _fold_leg "$l"; done
787
+ # A PROVISIONAL verdict — feedback has not fired yet, and it carries this in its
788
+ # payload. Recomputed once more below, after the fold, and THAT is the one the
789
+ # report and `--strict` read.
790
+ CLOSED=true
791
+ if [ "$FAIL" -gt 0 ] || [ "$UNRUN" -gt 0 ]; then CLOSED=false; fi
792
+
793
+ # ── settle the LAST run's decisions ─────────────────────────────────────────
794
+ # Before anything is filed. A leg this run reports `pass` vindicates whatever was
795
+ # auto-taken for it last time (mark, strength 1 — five of those make a highway);
796
+ # a leg still open refutes it (warn — one of those takes the authority straight
797
+ # back on the very next close). This is the only writer of the evidence the
798
+ # decide layer reads, and it writes from OUTCOMES, never from exit codes.
799
+ if [ "$DRY" -eq 0 ] && [ "$DECIDE" -eq 1 ] && [ -n "$PRIOR_DECIDED" ] && [ -z "${CLOSE_NO_DECIDE:-}" ]; then
800
+ while read -r _pl _po; do
801
+ [ -n "$_pl" ] || continue
802
+ _ps="$(_status_of "$_pl")"
803
+ case "$_ps" in pass) _out=pass ;; ""|skipped) continue ;; *) _out=open ;; esac
804
+ bash "$DECIDE_BIN" --settle --leg "$_pl" --status "$_po" \
805
+ --outcome "$_out" ${SLUG:+--slug "$SLUG"} >/dev/null 2>&1 || true
806
+ done <<PRIOR
807
+ $PRIOR_DECIDED
808
+ PRIOR
809
+ fi
810
+
811
+ DECIDED=0; RECOMMENDED=0; DECIDE_LINES=""; DECIDE_JSON_ROWS=""; DECIDE_ROWED=""
812
+ MARKED=0; WARNED=0; CLOSE_LEGS_JSON=""; CLOSE_DECIDE_JSON=""; CLOSE_TAGS=""
813
+ METRICS_TXT=""; METRICS_JSON=""; METRICS_S=0
814
+ # ── decide what a person does not need to be asked ──────────────────────────
815
+ # Runs AFTER the verdict and it is NOT a seventh leg: it cannot change `CLOSED`,
816
+ # it cannot turn anything red, and its exit code is discarded — the same law the
817
+ # metrics block obeys, for the same reason (a closer that can fail on its extras
818
+ # is a closer callers route around). What it changes is what gets FILED: a fork
819
+ # the substrate has proven is taken and the human is TOLD; a fork it has not is
820
+ # ranked, priced and RECOMMENDED, and the human still decides.
821
+ #
822
+ # It deliberately does not retro-flip a leg to `pass` when a decision closes it.
823
+ # The verdict above was computed at a real instant and reporting it otherwise
824
+ # would be the "unrun read as a pass" failure wearing a new hat. A taken decision
825
+ # says so in its own line and the next close measures the tree again.
826
+ if [ "$DECIDE" -eq 1 ] && [ -n "$OPEN_LEGS" ] && [ -z "${CLOSE_NO_DECIDE:-}" ] \
827
+ && [ -x "$DECIDE_BIN" ]; then
828
+ for l in $OPEN_LEGS; do
829
+ _wanted "$l" || continue
830
+ _dj=$(bash "$DECIDE_BIN" --leg "$l" --status "$(_status_of "$l")" \
831
+ --detail "$(_detail_of "$l")" ${SLUG:+--slug "$SLUG"} \
832
+ $([ "$DRY" -eq 0 ] && echo --apply) --json 2>/dev/null) || _drc=$?
833
+ case "${_drc:-0}" in
834
+ 5) _drc=0; continue ;; # no table row — file it as before
835
+ esac
836
+ _drc=0
837
+ [ -n "$_dj" ] || continue
838
+ _dres=$(printf '%s' "$_dj" | sed -n 's/.*"verdict":"\([a-z]*\)".*/\1/p')
839
+ _dch=$(printf '%s' "$_dj" | sed -n 's/.*"choice":"\([^"]*\)".*/\1/p')
840
+ _dcf=$(printf '%s' "$_dj" | sed -n 's/.*"confidence":"\([a-z]*\)".*/\1/p')
841
+ _dun=$(printf '%s' "$_dj" | sed -n 's/.*"undo":"\([^"]*\)".*/\1/p')
842
+ _drn=$(printf '%s' "$_dj" | sed -n 's/.*"reason":"\([^"]*\)".*/\1/p')
843
+ case "$_dres" in
844
+ taken)
845
+ DECIDED=$((DECIDED+1))
846
+ DECIDE_LINES="$DECIDE_LINES
847
+ $l → TAKEN $_dch (confidence: $_dcf) undo: $_dun"
848
+ DECIDE_JSON_ROWS="${DECIDE_JSON_ROWS:+$DECIDE_JSON_ROWS,}{\"leg\":\"$l\",\"id\":\"$(printf '%s' "$_dj" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')\",\"result\":\"taken\",\"choice\":\"$_dch\",\"confidence\":\"$_dcf\"}"
849
+ # The row records WHAT was taken, so the next close can settle it.
850
+ [ "$DRY" -eq 0 ] && { _ledger_row "$l" "$(_status_of "$l")" \
851
+ "$(_detail_of "$l") | decided: $_dch ($_dcf)" ledger "taken:$_dch"
852
+ DECIDE_ROWED="${DECIDE_ROWED:-} $l"; } ;;
853
+ veto)
854
+ DECIDE_LINES="$DECIDE_LINES
855
+ $l → HUMAN by rule: $_drn"
856
+ DECIDE_JSON_ROWS="${DECIDE_JSON_ROWS:+$DECIDE_JSON_ROWS,}{\"leg\":\"$l\",\"result\":\"veto\",\"reason\":\"$(_jsonesc "$_drn")\"}" ;;
857
+ *)
858
+ RECOMMENDED=$((RECOMMENDED+1))
859
+ DECIDE_LINES="$DECIDE_LINES
860
+ $l → RECOMMEND $_dch (confidence: $_dcf — unproven, ${DECIDE_N:-5} good outcomes make it automatic)"
861
+ DECIDE_JSON_ROWS="${DECIDE_JSON_ROWS:+$DECIDE_JSON_ROWS,}{\"leg\":\"$l\",\"result\":\"recommend\",\"choice\":\"$_dch\",\"confidence\":\"$_dcf\"}" ;;
862
+ esac
863
+ done
864
+ fi
865
+
866
+ # The human is the LAST channel, not the first. Ping when a leg reached neither
867
+ # the board nor anything anyone watches — or when --notify asks for it outright.
868
+ #
869
+ # ── measure ONCE, for both readers ──────────────────────────────────────────
870
+ # ~17s of real work (git plumbing, a cache-key hash, a governor read). It used to
871
+ # run at the very end, for the human only. Now it runs here, `--json-out` writes
872
+ # the machine copy, and stdout is held for the report — so the table a person
873
+ # reads and the numbers the signal carries are the SAME measurement of the same
874
+ # instant. On a tree seven sessions are editing, two runs are two different trees.
875
+ METRICS_TXT="$RUNDIR/metrics.txt"; METRICS_JSON="$RUNDIR/metrics.json"
876
+ if [ "$METRICS" -eq 1 ] && [ "$DRY" -eq 0 ] && [ -f "$ROOT/.claude/scripts/close-metrics.sh" ]; then
877
+ _MT0=$(date +%s)
878
+ _bounded 40 bash "$ROOT/.claude/scripts/close-metrics.sh" \
879
+ ${SLUG:+--slug "$SLUG"} ${COMPOSITE:+--composite "$COMPOSITE"} ${DIMS:+--dims "$DIMS"} \
880
+ --json-out "$METRICS_JSON" > "$METRICS_TXT" 2>/dev/null || true
881
+ METRICS_S=$(( $(date +%s) - _MT0 ))
882
+ fi
883
+
884
+ # ── the tags this close earned ──────────────────────────────────────────────
885
+ # Tags are how the world routes (root CLAUDE.md § the universal router): a signal
886
+ # carries tags and flows down the weighted Paths from those tags to whoever
887
+ # staked on them. A close that tags itself only `close,<slug>` is a close nobody
888
+ # can route on. These are DERIVED FROM WHAT WAS MEASURED, never declared — bare
889
+ # words, because a namespaced subscribe tag matches zero subscribers
890
+ # (subscribe-tags-parity.test.ts) and `lifecycle:<x>` is a different lane.
891
+ # TAGS ARE SANITISED AT THE DOOR, and it is not cosmetic. `land.sh` and
892
+ # `deploy.sh` pass a BRANCH NAME as the slug, so `feat/test-branch` became a tag,
893
+ # and the mark edge below is a URL PATH SEGMENT: the `/` split
894
+ # `/api/mark/tag%3Aone%3Afeat/test-branch…` into a route that does not exist.
895
+ # Measured 2026-09-09: 6 tags derived, 5 verbs landed, and the sixth failed
896
+ # silently — the close reported a number that looked fine. Bare word chars only,
897
+ # which is also the subscribe-tag rule (`subscribe-tags-parity.test.ts`).
898
+ _tag_add() {
899
+ local t; t=$(printf '%s' "$1" | tr '/' '-' | tr -cd 'a-zA-Z0-9_.:-')
900
+ [ -n "$t" ] || return 0
901
+ case " $CLOSE_TAGS " in *" $t "*) ;; *) CLOSE_TAGS="${CLOSE_TAGS:+$CLOSE_TAGS }$t" ;; esac
902
+ }
903
+ CLOSE_TAGS=""
904
+ for t in $(printf '%s' "${TAGS:-close,${SLUG:-session}}" | tr ',' ' '); do _tag_add "$t"; done
905
+ _tag_add close
906
+ [ "$CLOSED" = "true" ] && _tag_add closed || _tag_add unclosed
907
+ [ "$FAIL" -gt 0 ] && _tag_add failing
908
+ [ "$UNRUN" -gt 0 ] && _tag_add unrun
909
+ [ "$DECIDED" -gt 0 ] && _tag_add decided
910
+ [ "$RECOMMENDED" -gt 0 ] && _tag_add recommended
911
+ for l in gate derives task dims promise; do
912
+ case "$(_status_of "$l")" in fail|unrun) _tag_add "$l" ;; esac
913
+ done
914
+ # Speed is a tag too, so a drifting door is routable and not just readable.
915
+ _CLOSE_S=$(( $(date +%s) - T0 ))
916
+ # `slow` is judged on the LEGS, not on the measured block: the block is a fixed
917
+ # ~17s cost this script chose to pay, and tagging every close `slow` because of a
918
+ # constant is a tag that stops carrying information.
919
+ [ $(( _CLOSE_S - ${METRICS_S:-0} )) -gt 20 ] && _tag_add slow || _tag_add fast
920
+ [ -f "$METRICS_JSON" ] && grep -q '"state":"bad"' "$METRICS_JSON" 2>/dev/null && _tag_add degraded
921
+
922
+ # ── accretion: a signal gets RICHER at every node it crosses ────────────────
923
+ # The rule (the operator, 2026-09-09): "every time a signal hits a node we should
924
+ # add exponentially more metadata and data and images etc. to it." A signal that
925
+ # carries the same four fields at hop 5 as at hop 1 has learned nothing from the
926
+ # journey — and the journey is the only thing that knows what mattered.
927
+ #
928
+ # So the payload budget DOUBLES per hop: BASE << hop, and this close is one hop.
929
+ # `CLOSE_SIGNAL_HOP` is read from the caller (do-auto, land, deploy, the factory
930
+ # executor) and re-emitted incremented, so a signal crossing four nodes arrives
931
+ # with 16x the room it started with.
932
+ #
933
+ # TWO BOUNDS, because "exponentially more" without them is a signal nobody can
934
+ # store and every door refuses:
935
+ # 1. A HARD CEILING (ACCRETE_MAX, 256KB). The doubling is how fast the room
936
+ # grows, not a promise it grows forever.
937
+ # 2. BULK TRAVELS BY REFERENCE. Images, JSON dumps, diffs and gate packs go as
938
+ # {path, bytes, sha256} — verified to EXIST at emit time, never as bytes.
939
+ # That is what makes "and images etc." affordable: a screenshot costs ~120
940
+ # bytes of envelope, and the node that wants the pixels reads the path.
941
+ # Numbers are packed first, then details, then references — so a budget that runs
942
+ # out drops the biggest thing, never the verdict.
943
+ # THE TRAIL, AND THE WAY BACK. Accretion only pays if the enriched signal can be
944
+ # FOLDED BACK — otherwise every node is richer and the one who asked learns
945
+ # nothing. So the envelope carries three addresses and a trail:
946
+ #
947
+ # origin who started the work (survives every hop unchanged)
948
+ # sender the PREVIOUS node — one hop back
949
+ # reply_to where a return leg goes (defaults to sender, then origin)
950
+ # trail[] one entry per node crossed: hop, node, what it added, its tags
951
+ #
952
+ # Outbound the signal gains data; the return leg carries the accumulated whole
953
+ # back down the same path. That is the closed loop at the level of the JOURNEY
954
+ # rather than the single call — and `loop:feedback` was always the return leg;
955
+ # it just had nothing to return. `--reply` emits it.
956
+ ACCRETE_ORIGIN="${CLOSE_SIGNAL_ORIGIN:-${SLUG:-session}}"
957
+ ACCRETE_SENDER="${CLOSE_SIGNAL_SENDER:-}"
958
+ ACCRETE_REPLY="${CLOSE_REPLY_TO:-${ACCRETE_SENDER:-$ACCRETE_ORIGIN}}"
959
+ ACCRETE_HOP=$(( ${CLOSE_SIGNAL_HOP:-0} + 1 ))
960
+ ACCRETE_BASE="${ACCRETE_BASE:-4096}"
961
+ ACCRETE_MAX="${ACCRETE_MAX:-262144}"
962
+ ACCRETE_BUDGET=$(( ACCRETE_BASE << (ACCRETE_HOP > 6 ? 6 : ACCRETE_HOP) ))
963
+ [ "$ACCRETE_BUDGET" -gt "$ACCRETE_MAX" ] && ACCRETE_BUDGET="$ACCRETE_MAX"
964
+
965
+ # Every artifact this close can point at, as a reference. `test -s` before each,
966
+ # because a reference to a file that is not there is the "file exists is theater"
967
+ # failure with an extra hop of delay on it.
968
+ _ref() { # <kind> <path>
969
+ [ -s "$2" ] || return 0
970
+ local sz sha
971
+ sz=$(wc -c < "$2" 2>/dev/null | tr -d ' ')
972
+ sha=$(shasum -a 256 "$2" 2>/dev/null | cut -c1-16)
973
+ printf '%s{"kind":"%s","path":"%s","bytes":%s,"sha256":"%s"}' \
974
+ "${_rc_:-}" "$1" "$(_jsonesc "${2#$ROOT/}")" "${sz:-0}" "${sha:-}"; _rc_=,
975
+ }
976
+ ARTIFACTS=$( _rc_=""
977
+ _ref metrics "$METRICS_JSON"
978
+ _ref metrics_text "$METRICS_TXT"
979
+ _ref ledger "$LEDGER"
980
+ _ref w4_gates "$ROOT/.w4-gates.json"
981
+ _ref w2_spec "$ROOT/.w2-spec.json"
982
+ _ref promise "$ROOT/text/${SLUG:-__none__}.md"
983
+ _ref todo "$ROOT/text/${SLUG:-__none__}-todo.md"
984
+ _ref plan "$ROOT/text/${SLUG:-__none__}-plan.md"
985
+ _ref deploy_runs "$ROOT/one.ie/web/src/data/deploy-runs.json"
986
+ # IMAGES. do-prove.sh / chrome.mjs leave screenshots behind; they are the only
987
+ # evidence in this whole payload that a human can check at a glance.
988
+ for _img in "$ROOT"/.claude/.artifacts/*.png "$ROOT"/.playwright-mcp/*.png "/tmp/one-prove"/*.png; do
989
+ [ -s "$_img" ] || continue; _ref screenshot "$_img"
990
+ done )
991
+
992
+ # The diff itself, by reference AND — if the budget stretches — by value. The
993
+ # close already knows what changed; a node downstream that must judge the change
994
+ # should not have to re-derive it from a sha.
995
+ DIFFSTAT=$(git -C "$ROOT" diff HEAD --stat 2>/dev/null | tail -40 || true)
996
+ [ "${#DIFFSTAT}" -gt "$(( ACCRETE_BUDGET / 4 ))" ] && DIFFSTAT="${DIFFSTAT:0:$(( ACCRETE_BUDGET / 4 ))}"
997
+
998
+ # This node's entry, appended to whatever the caller handed us. The inbound trail
999
+ # is passed through VERBATIM — a node that rewrites the history it was given is a
1000
+ # node whose trail cannot be trusted; it may only append.
1001
+ _ACC_ADDED=$(( ${#ARTIFACTS} + ${#DIFFSTAT} + ${#CLOSE_LEGS_JSON} ))
1002
+ TRAIL_ENTRY=$(printf '{"hop":%d,"node":"close:%s","at":"%s","added_bytes":%d,"artifacts":%d,"tags":"%s","verdict":"%s"}' \
1003
+ "$ACCRETE_HOP" "${SLUG:-session}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$_ACC_ADDED" \
1004
+ "$(printf '%s' "$ARTIFACTS" | grep -o '"kind"' | grep -c . || echo 0)" \
1005
+ "$(_jsonesc "$CLOSE_TAGS")" "$CLOSED")
1006
+ TRAIL="${CLOSE_SIGNAL_TRAIL:+${CLOSE_SIGNAL_TRAIL},}$TRAIL_ENTRY"
1007
+
1008
+ # ── wave 3: the return path, carrying the whole close ───────────────────────
1009
+ CLOSE_LEGS_JSON=$(for l in $LEGS_ALL; do
1010
+ printf '%s{"leg":"%s","status":"%s","secs":%s,"detail":"%s"}' \
1011
+ "${_c:-}" "$l" "$(_status_of "$l")" "$(_secs_of "$l")" "$(_jsonesc "$(_detail_of "$l")")"; _c=,
1012
+ done)
1013
+ CLOSE_DECIDE_JSON=$(printf '%s' "${DECIDE_JSON_ROWS:-}")
1014
+ _run_leg feedback
1015
+ _fold_leg feedback
1016
+
1017
+ # ── the verdict, final ──────────────────────────────────────────────────────
1018
+ # `closed` is true only with ZERO fails and ZERO unruns — do-w4-gates.sh's
1019
+ # deterministic_pass, same law: an unrun leg is not a closed leg.
1020
+ CLOSED=true
1021
+ if [ "$FAIL" -gt 0 ] || [ "$UNRUN" -gt 0 ]; then CLOSED=false; fi
1022
+
1023
+ # ── escalate what did not close ─────────────────────────────────────────────
1024
+ FILED=0; UNFILED=0; LEDGERED=0
1025
+ if [ "$DRY" -eq 0 ] && [ -z "${CLOSE_NO_ESCALATE:-}" ]; then
1026
+ for l in $OPEN_LEGS; do
1027
+ # An --only run must not file rows for legs it deliberately did not run.
1028
+ _wanted "$l" || continue
1029
+ # A leg the decide layer already rowed carries its `taken:<option>`, and that
1030
+ # field is what the NEXT close settles against. A plain escalation row here
1031
+ # would silently erase it (the ledger is a keyed union — last writer wins),
1032
+ # and the decision would never be marked or warned. The board task still
1033
+ # gets filed below for those legs; only the ledger write is skipped.
1034
+ case " ${DECIDE_ROWED:-} " in *" $l "*) _file_task "$l" "$(_status_of "$l")" "$(_detail_of "$l")" && FILED=$((FILED+1)); continue ;; esac
1035
+ if _file_task "$l" "$(_status_of "$l")" "$(_detail_of "$l")"; then
1036
+ _ledger_row "$l" "$(_status_of "$l")" "$(_detail_of "$l")" task; FILED=$((FILED+1))
1037
+ else
1038
+ _ledger_row "$l" "$(_status_of "$l")" "$(_detail_of "$l")" ledger
1039
+ LEDGERED=$((LEDGERED+1))
1040
+ # --no-file is the operator choosing ledger-only. That is not an unreached
1041
+ # board, so it must not page anyone: a notifier that fires when it was told
1042
+ # not to file is a notifier the human mutes. UNFILED counts only the rows
1043
+ # that reached NOTHING — it is the trigger for the human, and conflating it
1044
+ # with "written to the ledger" reported `ledger=0` on a run that had just
1045
+ # written two rows.
1046
+ [ "$NOFILE" -eq 1 ] || UNFILED=$((UNFILED+1))
1047
+ fi
1048
+ done
1049
+ elif [ "$DRY" -eq 0 ]; then
1050
+ # Same rule as the branch above: never overwrite a row the decide layer wrote,
1051
+ # or its `taken:<option>` is lost and the decision is never settled.
1052
+ for l in $OPEN_LEGS; do
1053
+ _wanted "$l" || continue
1054
+ case " ${DECIDE_ROWED:-} " in *" $l "*) continue ;; esac
1055
+ _ledger_row "$l" "$(_status_of "$l")" "$(_detail_of "$l")" ledger; LEDGERED=$((LEDGERED+1))
1056
+ done
1057
+ fi
1058
+
1059
+ # ── the return leg: fold the enriched signal back to who asked ──────────────
1060
+ # Accretion without a fold is a signal that gets richer while the one who asked
1061
+ # learns nothing. `--reply` sends the SAME envelope back one hop — to `sender`,
1062
+ # or to `origin` when this is hop 1 — tagged `reply` and `for:<node>` so the
1063
+ # router delivers it to whoever staked on that node rather than broadcasting.
1064
+ #
1065
+ # It is the outbound payload verbatim, not a summary: the sender asked for work,
1066
+ # and what comes back is everything the journey learned, including the artifact
1067
+ # references it can now read for itself. A summary here would be this node
1068
+ # deciding what the previous node is allowed to know.
1069
+ #
1070
+ # Bounded and fire-and-forget, like every write in this file — a return leg that
1071
+ # can fail the close is a return leg callers switch off.
1072
+ REPLIED=none
1073
+ if [ "$DRY" -eq 0 ] && [ "$REPLY" -eq 1 ] && [ -n "$ACCRETE_REPLY" ] && [ -n "${FEEDBACK_BODY:-}" ]; then
1074
+ _rbody=$(printf '%s' "$FEEDBACK_BODY" \
1075
+ | sed "s/\"tags\":\[/\"tags\":[\"reply\",\"for:$(printf '%s' "$ACCRETE_REPLY" | tr -c 'a-zA-Z0-9-' '-')\",/")
1076
+ if _post "$BASE/api/signal/loop:feedback" "$_rbody" 6 >/dev/null 2>&1; then REPLIED="$ACCRETE_REPLY"
1077
+ else REPLIED=unreached; fi
1078
+ fi
1079
+
1080
+ # ── mark · warn — the close closes its own loop ─────────────────────────────
1081
+ # Rule 1: every signal closes with mark() or warn(). The close has been emitting
1082
+ # a feedback signal for months and depositing NOTHING, so the tags it travels on
1083
+ # never learned whether travelling on them went well. Now each derived tag gets
1084
+ # one verb on the edge `tag:<workspace>:<tag> → close:<outcome>`, carrying the
1085
+ # whole tag set — so `mark` on a closed run and `warn` on an open one moves the
1086
+ # same weighted Paths the router reads, and a tag that keeps riding bad closes
1087
+ # stops out-ranking one that does not.
1088
+ #
1089
+ # closed -> mark strength 1
1090
+ # open -> warn strength 1 (a fail or an unrun; both are the loop not closed)
1091
+ # timeout -> NEITHER — a held lease is neutral by rule, the same rule that makes
1092
+ # the task and dims legs n/a on a timeout. A halt is not an outcome.
1093
+ #
1094
+ # Strength 1 and a `tag:` source, deliberately disjoint from the `decide:close:*`
1095
+ # sources do-decide.sh reads: these marks must never become confidence for an
1096
+ # auto-decision, or the close would be voting on its own authority.
1097
+ MARKED=0; WARNED=0
1098
+ if [ "$DRY" -eq 0 ] && [ "$STATUS" != "timeout" ] && [ -n "$CLOSE_TAGS" ]; then
1099
+ _verb=mark; [ "$CLOSED" = "true" ] || _verb=warn
1100
+ _tgt="close:$( [ "$CLOSED" = "true" ] && echo kept || echo open )"
1101
+ _tagj=$(printf '%s' "$CLOSE_TAGS" | awk '{for(i=1;i<=NF;i++){printf "%s\"%s\"",(i>1?",":""),$i}}')
1102
+ for t in $CLOSE_TAGS; do
1103
+ _edge=$(printf 'tag:%s:%s→%s' "$WORKSPACE" "$t" "$_tgt" | sed 's/:/%3A/g; s/→/%E2%86%92/g')
1104
+ if _post "$BASE/api/$_verb/$_edge" "{\"strength\":1,\"tags\":[$_tagj]}" 4 >/dev/null 2>&1; then
1105
+ [ "$_verb" = mark ] && MARKED=$((MARKED+1)) || WARNED=$((WARNED+1))
1106
+ fi
1107
+ done
1108
+ fi
1109
+
1110
+ # A TAKEN DECISION IS ALWAYS ANNOUNCED, even when every row reached the board.
1111
+ # That is the whole trade this layer makes: the human's veto moves from BEFORE
1112
+ # the action to AFTER it, and that is only honest if they actually hear about it
1113
+ # and hold the reversal. So `DECIDED > 0` pages on its own terms — not as a
1114
+ # question, as a receipt with an undo line on it. A recommendation never pages;
1115
+ # it is already on the board and in the report, and a notifier that fires for
1116
+ # things nobody must act on is a notifier the human mutes.
1117
+ NOTIFIED=none
1118
+ if [ "$DRY" -eq 0 ] && { [ "$UNFILED" -gt 0 ] || [ "$NOTIFY" -eq 1 ] || [ "$DECIDED" -gt 0 ]; }; then
1119
+ if _notify_human "close ${SLUG:-session}: $UNRUN unrun, $FAIL fail, $UNFILED unfiled, $DECIDED decided —$OPEN_LEGS
1120
+ $(for l in $OPEN_LEGS; do printf ' %s\n' "$(_fix_for "$l")"; done)${DECIDE_LINES}"; then NOTIFIED=sent; else NOTIFIED=failed; fi
1121
+ fi
1122
+
1123
+ # ── report ──────────────────────────────────────────────────────────────────
1124
+ ELAPSED=$(( $(date +%s) - T0 ))
1125
+ if [ "$JSON" -eq 1 ]; then
1126
+ printf '{"slug":"%s","closed":%s,"elapsed_s":%d,"legs":{' "${SLUG:-}" "$CLOSED" "$ELAPSED"
1127
+ first=1
1128
+ for l in $LEGS_ALL; do
1129
+ [ "$first" -eq 1 ] || printf ','; first=0
1130
+ printf '"%s":{"status":"%s","secs":%s,"detail":"%s"}' "$l" "$(_status_of "$l")" "$(_secs_of "$l")" "$(_jsonesc "$(_detail_of "$l")")"
1131
+ done
1132
+ printf '},"pass":%d,"fail":%d,"unrun":%d,"na":%d,"filed":%d,"ledgered":%d,"unreached":%d,"notified":"%s","decided":%d,"recommended":%d,"marked":%d,"warned":%d,"tags":"%s"}\n' \
1133
+ "$PASS" "$FAIL" "$UNRUN" "$NA" "$FILED" "$LEDGERED" "$UNFILED" "$NOTIFIED" "$DECIDED" "$RECOMMENDED" \
1134
+ "${MARKED:-0}" "${WARNED:-0}" "$(_jsonesc "$CLOSE_TAGS")"
1135
+ else
1136
+ _tag=""; [ "$DRY" -eq 1 ] && _tag=" (DRY-RUN)"
1137
+ echo "── close ${SLUG:-session}${_tag} ──"
1138
+ for l in $LEGS_ALL; do printf ' %-9s %-6s %3ss %s\n' "$l" "$(_status_of "$l")" "$(_secs_of "$l")" "$(_detail_of "$l")"; done
1139
+ printf ' closed=%s pass=%d fail=%d unrun=%d n/a=%d %ds door=%s\n' \
1140
+ "$CLOSED" "$PASS" "$FAIL" "$UNRUN" "$NA" "$ELAPSED" "$BASE"
1141
+ # The clock, split. "the close was slow" and "the promise proof was slow" are
1142
+ # different facts with different fixes, and the measured block is neither a leg
1143
+ # nor free — naming its seconds separately is what stops it being blamed on a door.
1144
+ printf ' speed: %s| metrics=%ss total=%ss\n' \
1145
+ "$(for l in $LEGS_ALL; do printf '%s=%ss ' "$l" "$(_secs_of "$l")"; done)" "${METRICS_S:-0}" "$ELAPSED"
1146
+ printf ' tags: %s\n' "${CLOSE_TAGS:-none}"
1147
+ printf ' trail: hop=%s origin=%s sender=%s reply=%s budget=%sB artifacts=%s\n' \
1148
+ "${ACCRETE_HOP:-1}" "${ACCRETE_ORIGIN:-}" "${ACCRETE_SENDER:--}" "${REPLIED:-none}" \
1149
+ "${ACCRETE_BUDGET:-0}" "$(printf '%s' "${ARTIFACTS:-}" | grep -o '"kind"' | grep -c . || echo 0)"
1150
+ printf ' verbs: mark=%d warn=%d on tag:%s:<tag>→close:%s\n' "${MARKED:-0}" "${WARNED:-0}" \
1151
+ "$WORKSPACE" "$( [ "$CLOSED" = "true" ] && echo kept || echo open )"
1152
+ if [ -n "$OPEN_LEGS" ] && [ "$DRY" -eq 0 ]; then
1153
+ printf ' open: filed=%d task(s) ledger=%d row(s) unreached=%d human=%s\n' "$FILED" "$LEDGERED" "$UNFILED" "$NOTIFIED"
1154
+ for l in $OPEN_LEGS; do _wanted "$l" && printf ' %s → %s\n' "$l" "$(_fix_for "$l")"; done
1155
+ printf ' see them again: bash .claude/scripts/do-close.sh --pending\n'
1156
+ fi
1157
+ # The decisions, under the blockers they belong to. A TAKEN line always carries
1158
+ # its undo; a RECOMMEND line always carries the ranked options and their cost,
1159
+ # because a recommendation you cannot argue with is just a slower ask.
1160
+ if [ -n "$DECIDE_LINES" ]; then
1161
+ printf ' decide: taken=%d recommended=%d (highway = 5 proven outcomes; one bad one revokes it)%s\n' \
1162
+ "$DECIDED" "$RECOMMENDED" "$DECIDE_LINES"
1163
+ fi
1164
+
1165
+ # ── the numbers ───────────────────────────────────────────────────────────
1166
+ # ADDITIVE, NEVER A SEVENTH LEG, and that distinction is the whole design. The
1167
+ # six legs above decide `closed`; these rows decide nothing and can turn
1168
+ # nothing red. A metric that could fail a close would make this a closer people
1169
+ # route around — and the numbers would leave with it. So it runs AFTER the
1170
+ # verdict, inside its own budget, and its exit code is discarded on purpose.
1171
+ #
1172
+ # `/close` reported six leg states and almost no numbers about the WORK.
1173
+ # "closed=true pass=6" says the doors were knocked on. It does not say what
1174
+ # changed, whether this exact tree is proven, whether the branch reached trunk,
1175
+ # what is still open, or WHY something could not be measured. close-metrics.sh
1176
+ # composes those from the scripts that already own them.
1177
+ # Printed from the run captured above — NEVER re-run here. Two runs of a ~17s
1178
+ # measurement are two different trees on a box seven sessions are editing, and
1179
+ # the signal would then carry numbers the human never saw.
1180
+ if [ -s "$METRICS_TXT" ]; then sed '1d' "$METRICS_TXT"; fi
1181
+ fi
1182
+
1183
+ [ "$STRICT" -eq 1 ] && [ "$CLOSED" = "false" ] && exit 1
1184
+ exit 0