@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- 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
|