@oneie/claude 0.7.0 → 0.9.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 (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. package/scripts/do-tasks-bridge.py +0 -366
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env bash
2
+ # do-triage.sh — zero-LLM pre-pass over a captured task.
3
+ # Emits {verdict, paths, route, dupe}. Verdicts: sizeable | idea | dupe | split.
4
+ # Discriminator for idea: can you state in one line the check that proves this done?
5
+ # Usage: do-triage.sh "captured text" | do-triage.sh --self-test
6
+ set -euo pipefail
7
+
8
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
9
+ TEXT_DIR="${TRIAGE_TEXT_DIR:-$ROOT/text}"
10
+ PAGES="$ROOT/one.ie/web/src/pages"
11
+
12
+ # --self-test — one fixture per verdict (same idiom as do-tier.sh).
13
+ if [ "${1:-}" = "--self-test" ]; then
14
+ S="$(cd "$(dirname "$0")" && pwd)"; f=0
15
+ export TRIAGE_TEXT_DIR="$S/fixtures"
16
+ chk() {
17
+ want="$1"; shift
18
+ got="$("$S/do-triage.sh" "$@" 2>/dev/null)" || true
19
+ case "$got" in *"\"verdict\":\"$want\""*) printf ' ok %s → %s\n' "$*" "$want" ;;
20
+ *) printf ' FAIL %s → expected %s, got %s\n' "$*" "$want" "$got"; f=1 ;; esac
21
+ }
22
+ chk sizeable "fix UNSIZED exit in .claude/scripts/do-tier.sh"
23
+ chk idea "make the board nicer"
24
+ chk split "fix .claude/scripts/do-tier.sh and .claude/scripts/do-survey.sh"
25
+ chk dupe "xyzzy-triage-dupe unique deliverable phrase"
26
+ # sizeable names a real file — paths must include it (feeds do-tier, never a sentence)
27
+ got="$("$S/do-triage.sh" "fix UNSIZED exit in .claude/scripts/do-tier.sh" 2>/dev/null)" || true
28
+ case "$got" in *".claude/scripts/do-tier.sh"*) printf ' ok sizeable paths include the named file\n' ;;
29
+ *) printf ' FAIL sizeable paths missing named file: %s\n' "$got"; f=1 ;; esac
30
+ got="$("$S/do-triage.sh" "the banner on /chat shows twice" 2>/dev/null)" || true
31
+ case "$got" in *"\"verdict\":\"sizeable\""*"\"route\":\"/chat\""*|*"\"route\":\"/chat\""*"\"verdict\":\"sizeable\""*) printf ' ok named route /chat → sizeable\n' ;;
32
+ *) printf ' FAIL named route /chat: %s\n' "$got"; f=1 ;; esac
33
+ [ "$f" -eq 0 ] && echo "do-triage: self-test green" || echo "do-triage: self-test FAILED"
34
+ exit "$f"
35
+ fi
36
+
37
+ text="${*:-}"
38
+ if [ -z "$text" ]; then
39
+ if [ -t 0 ]; then echo "usage: do-triage.sh <captured text> | --self-test" >&2; exit 4; fi
40
+ text="$(cat)"
41
+ fi
42
+ [ -z "$text" ] && { echo "usage: do-triage.sh <captured text> | --self-test" >&2; exit 4; }
43
+
44
+ # Existing repo-relative paths the title names.
45
+ paths=""
46
+ while IFS= read -r p; do
47
+ [ -z "$p" ] && continue
48
+ case "$p" in http*|*:*) continue ;; esac
49
+ if [ -e "$ROOT/$p" ]; then
50
+ case "
51
+ $paths
52
+ " in *"
53
+ $p
54
+ "*) ;; *) paths="${paths}${p}
55
+ " ;; esac
56
+ fi
57
+ done <<EOF
58
+ $(printf '%s' "$text" | grep -oE '[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)+' || true)
59
+ EOF
60
+
61
+ # A whitespace-bounded /route that maps onto an existing page.
62
+ route=""
63
+ while IFS= read -r tok; do
64
+ [ -z "$tok" ] && continue
65
+ r="$(printf '%s' "$tok" | sed 's/^[[:space:]]*//')"
66
+ rel="${r#/}"
67
+ page=""
68
+ if [ -f "$PAGES/${rel}.astro" ]; then page="one.ie/web/src/pages/${rel}.astro"
69
+ elif [ -f "$PAGES/${rel}/index.astro" ]; then page="one.ie/web/src/pages/${rel}/index.astro"
70
+ else
71
+ slugrel="$(printf '%s' "$rel" | sed -E 's|^u/[^/]+/|u/[slug]/|')"
72
+ if [ "$slugrel" != "$rel" ]; then
73
+ [ -f "$PAGES/${slugrel}.astro" ] && page="one.ie/web/src/pages/${slugrel}.astro"
74
+ [ -z "$page" ] && [ -f "$PAGES/${slugrel}/index.astro" ] && page="one.ie/web/src/pages/${slugrel}/index.astro"
75
+ fi
76
+ fi
77
+ if [ -n "$page" ]; then
78
+ route="$r"
79
+ case "
80
+ $paths
81
+ " in *"
82
+ $page
83
+ "*) ;; *) paths="${paths}${page}
84
+ " ;; esac
85
+ break
86
+ fi
87
+ done <<EOF
88
+ $(printf '%s' "$text" | grep -oE '(^|[[:space:]])/[a-z][][a-z0-9_/-]*' || true)
89
+ EOF
90
+
91
+ # Near-dupe of an existing promise deliverable (or optional open-task list).
92
+ # One Python walk — a bash for-loop over text/*.md is ~8s (1470 files) and find hits ARG_MAX.
93
+ dupe_json="null"
94
+ if [ -d "$TEXT_DIR" ]; then
95
+ dupe_json="$(python3 - "$TEXT_DIR" "$ROOT" "$text" "${TRIAGE_OPEN_TASKS:-}" <<'PY'
96
+ import json, os, re, sys
97
+ text_dir, root, needle, open_tasks = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
98
+ def norm(s):
99
+ return re.sub(r"\s+", " ", s.lower()).strip()
100
+ def unquote(s):
101
+ s = s.strip()
102
+ if len(s) >= 2 and s[0] == '"':
103
+ s = re.sub(r'^"(.*)"\s*(#.*)?$', r"\1", s).replace('\\"', '"')
104
+ elif len(s) >= 2 and s[0] == "'":
105
+ s = re.sub(r"^'(.*)'\s*(#.*)?$", r"\1", s)
106
+ else:
107
+ s = re.sub(r"\s+#.*$", "", s)
108
+ return s
109
+ nt = norm(needle)
110
+ item_re = re.compile(r"^\s*-\s*item:\s*(.*)$")
111
+ def hit(item, rel):
112
+ nitem = norm(item)
113
+ if len(nitem) < 16:
114
+ return False
115
+ if nitem in nt or (len(nt) >= 16 and nt in nitem):
116
+ print(json.dumps({"item": item, "file": rel}, separators=(",", ":")))
117
+ return True
118
+ return False
119
+ for dirpath, dirnames, files in os.walk(text_dir):
120
+ dirnames[:] = [d for d in dirnames if not d.startswith(".")]
121
+ rel_dir = os.path.relpath(dirpath, text_dir)
122
+ depth = 0 if rel_dir == "." else rel_dir.count(os.sep) + 1
123
+ if depth > 2:
124
+ dirnames[:] = []
125
+ continue
126
+ for fn in files:
127
+ if not fn.endswith(".md"):
128
+ continue
129
+ path = os.path.join(dirpath, fn)
130
+ try:
131
+ with open(path, encoding="utf-8", errors="ignore") as fh:
132
+ in_fm = 0
133
+ for line in fh:
134
+ if line.startswith("---"):
135
+ in_fm += 1
136
+ if in_fm >= 2:
137
+ break
138
+ continue
139
+ if in_fm != 1:
140
+ continue
141
+ m = item_re.match(line)
142
+ if not m:
143
+ continue
144
+ item = unquote(m.group(1))
145
+ rel = path[len(root)+1:] if path.startswith(root) else path
146
+ if hit(item, rel):
147
+ sys.exit(0)
148
+ except OSError:
149
+ continue
150
+ if open_tasks and os.path.isfile(open_tasks):
151
+ for line in open(open_tasks, encoding="utf-8", errors="ignore"):
152
+ ot = line.strip()
153
+ if ot and hit(ot, "open-task"):
154
+ sys.exit(0)
155
+ print("null")
156
+ PY
157
+ )"
158
+ fi
159
+
160
+ nfiles=0
161
+ if [ -n "$paths" ]; then nfiles="$(printf '%s' "$paths" | grep -c . || true)"; fi
162
+ nnum="$(printf '%s' "$text" | grep -oE '(^|[[:space:]])[0-9]+\.' | grep -c . || true)"
163
+ has_cmd=0
164
+ printf '%s' "$text" | grep -qE 'test -[efd]|grep -[qF]|bunx?[[:space:]]+vitest|bash[[:space:]]|\.sh[[:space:]]|accept:' && has_cmd=1 || true
165
+
166
+ verdict=idea
167
+ if [ "$dupe_json" != "null" ]; then
168
+ verdict=dupe
169
+ elif [ "$nfiles" -ge 2 ] || [ "$nnum" -ge 2 ]; then
170
+ verdict=split
171
+ elif [ "$nfiles" -ge 1 ] || [ -n "$route" ] || [ "$has_cmd" -eq 1 ]; then
172
+ verdict=sizeable
173
+ fi
174
+
175
+ python3 -c '
176
+ import json,sys
177
+ verdict, route, dupe = sys.argv[1], sys.argv[2], sys.argv[3]
178
+ paths = [ln for ln in sys.stdin.read().splitlines() if ln]
179
+ print(json.dumps({"verdict":verdict,"paths":paths,"route":route or None,"dupe":None if dupe=="null" else json.loads(dupe)}, separators=(",",":")))
180
+ ' "$verdict" "$route" "$dupe_json" <<EOF
181
+ $paths
182
+ EOF
@@ -311,7 +311,7 @@ for n in $active_ids; do
311
311
  if [ -n "$r" ] && [ -n "$base" ]; then
312
312
  body="$(curl -s --max-time 20 "$base$r" 2>/dev/null || true)"
313
313
  if [ -z "$body" ]; then ok=0; why="no response from $base$r"; fi
314
- if [ "$ok" -eq 1 ] && [ -n "$et" ] && ! printf '%s' "$body" | grep -qiF "$et"; then
314
+ if [ "$ok" -eq 1 ] && [ -n "$et" ] && ! grep -qiF "$et" <<<"$body"; then
315
315
  ok=0; why="text '$et' not in served HTML"
316
316
  fi
317
317
  fi
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env bash
2
+ # do-w4-gates.sh — run W4's DETERMINISTIC gate battery, emit one JSON evidence pack.
3
+ #
4
+ # manifest: needs-env
5
+ #
6
+ # needs-env: it runs do-reconcile.sh, do-folder.sh and the folder verify lane,
7
+ # all of which are needs-env.
8
+ #
9
+ # WHY. Every gate in `.claude/commands/do.md` § W4 is already bash. The `w4-verify`
10
+ # agent was being spawned (28.9KB of prose + 5.4KB of w4-tools.md) mostly to READ
11
+ # those gate lines out of the markdown and then type them into Bash. That is a
12
+ # model call to operate a shell. This script IS the gate list, executed, with a
13
+ # machine-readable receipt — so the only thing left that needs judgment is the
14
+ # rubric VERDICT (goal-fit especially, which no script can score).
15
+ #
16
+ # GATE PARITY IS THE WHOLE POINT. This script must run exactly the gates do.md
17
+ # lists, no more and no fewer. It adds nothing and removes nothing; it only
18
+ # stops a model from re-typing them.
19
+ #
20
+ # THE HONESTY LAW (this repo has shipped green gates over red trees three times
21
+ # on record): a gate has FOUR states, never two —
22
+ # pass : ran, exit 0
23
+ # fail : ran, non-zero
24
+ # unrun : could not run (tool absent, verify skipped, no baseline)
25
+ # n/a : does not apply to this diff (by rule, not by accident)
26
+ # `deterministic_pass` is true only when there are ZERO fails and ZERO unruns.
27
+ # An unrun gate is NOT a passed gate. An absent tool is an unrun check, never a
28
+ # green one.
29
+ #
30
+ # TWO GATES THIS SCRIPT CANNOT INVENT. do.md's W4 block also runs the cycle's
31
+ # `$(cycle.demo.command)` — the goal gate — and `do-prove.sh <slug>` when the
32
+ # promise exists. Neither is derivable from a diff: one lives in the cycle, the
33
+ # other needs the plan slug. So the CALLER supplies them (`--demo`, `--slug`)
34
+ # and, when it does not, they are reported `unrun` — which under the law above
35
+ # makes `deterministic_pass` false. Running this script bare therefore CANNOT
36
+ # produce a green over an unrun goal gate. That is deliberate, not an oversight.
37
+ #
38
+ # Usage:
39
+ # do-w4-gates.sh [--files <changed-path>...] # default: git diff HEAD --name-only
40
+ # [--demo '<command>'] # the cycle's goal gate
41
+ # [--slug <slug>] # enables do-prove.sh <slug>
42
+ # do-w4-gates.sh --self-test # fixtures + RED PROOF
43
+ # Env:
44
+ # W4_SKIP_VERIFY=1 skip the folder verify lane -> reported `unrun` (never pass)
45
+ # W4_SKIP_RECONCILE=1 SELF-TEST ONLY — skip the 7 canons + consumer sweep, all
46
+ # reported `unrun`. Exists because `do-reconcile.sh types`
47
+ # is a tsc that can queue for minutes behind the governor
48
+ # and the self-test calls this script six times. Safe only
49
+ # because a skip is unrun, not pass; never set it for real.
50
+ set -euo pipefail
51
+
52
+ # _GATE — see governor-doors-check.sh "compute sites". A gate_lock dedupes
53
+ # identical work; only a SLOT bounds N worktrees running N computes at once.
54
+ # Empty inside an existing gate so the outer slot is inherited, never doubled.
55
+ _GATE=()
56
+ if [ "${GOVERN_IN_GATE:-0}" != "1" ]; then
57
+ _GR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
58
+ [ -f "$_GR" ] && _GATE=( bash "$_GR" "compute:$(basename "${BASH_SOURCE[0]}")" -- )
59
+ fi
60
+
61
+
62
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
63
+ S="$ROOT/.claude/scripts"
64
+ RESULTS_FILE=""
65
+ DEMO_CMD=""
66
+ SLUG=""
67
+
68
+ _add() { # name status detail
69
+ jq -n --arg n "$1" --arg s "$2" --arg d "$3" '{gate:$n,status:$s,detail:$d}' >> "$RESULTS_FILE"
70
+ }
71
+
72
+ # Run a command; classify by exit code. Never let a missing tool invert the exit.
73
+ _run() { # name cmd...
74
+ local name="$1"; shift
75
+ local out rc
76
+ set +e
77
+ out="$( "$@" </dev/null 2>&1 )"; rc=$?
78
+ set -e
79
+ if [ $rc -eq 0 ]; then _add "$name" pass "$(printf '%s' "$out" | tail -3 | tr '\n' ' ')"
80
+ else _add "$name" fail "exit=$rc $(printf '%s' "$out" | tail -5 | tr '\n' ' ')"; fi
81
+ }
82
+
83
+ _gates() {
84
+ # macOS ships bash 3.2: no `mapfile`, and "${arr[@]}" on an EMPTY array is an
85
+ # unbound-variable error under `set -u`. So the changed set is carried as a
86
+ # newline-delimited STRING and re-split only where a command needs argv.
87
+ local files_txt nfiles
88
+ if [ "$#" -gt 0 ]; then files_txt="$(printf '%s\n' "$@")"
89
+ else files_txt="$(git -C "$ROOT" diff HEAD --name-only 2>/dev/null || true)"; fi
90
+ nfiles="$(printf '%s\n' "$files_txt" | grep -c . || true)"
91
+ RESULTS_FILE="$(mktemp)"
92
+
93
+ # ---- 1. folder verify lane (bun run verify / verify:fast; doc-only -> n/a) ----
94
+ local folders
95
+ folders="$(printf '%s\n' "$files_txt" | bash "$S/do-folder.sh" 2>/dev/null || echo '')"
96
+ if [ -z "$folders" ]; then
97
+ _add verify unrun "do-folder.sh resolved no folder for the changed set"
98
+ elif printf '%s\n' "$folders" | jq -se 'all(.doc_only)' >/dev/null 2>&1; then
99
+ _add verify n/a "doc-only diff — do.md W0/W4 skip bun verify by rule"
100
+ elif [ "${W4_SKIP_VERIFY:-0}" = "1" ]; then
101
+ _add verify unrun "W4_SKIP_VERIFY=1 — an unrun gate is not a pass"
102
+ else
103
+ local f v
104
+ while IFS=$'\t' read -r f v; do
105
+ [ -n "$f" ] || continue
106
+ _run "verify:$f" bash -c "cd '$ROOT/$f' && $v"
107
+ done < <(printf '%s\n' "$folders" | jq -r 'select(.verify) | "\(.folder)\t\(.verify)"')
108
+ # A folder with NO verify script must say so. `select(.verify)` silently
109
+ # drops it, so before 2026-09-03 a changed set that resolved only to
110
+ # verify-less folders ran the loop zero times and added NO ROW AT ALL --
111
+ # not pass, not unrun, not n/a. That is outside this script's own four-state
112
+ # vocabulary, and an absent row is the fail-open trap in data form: every
113
+ # reader that asks "did verify pass" gets nothing back and treats it as
114
+ # nothing to worry about. Measured on channels/ and pay/, two of the five
115
+ # deployed services, in BOTH lanes.
116
+ local _unverified
117
+ _unverified="$(printf '%s\n' "$folders" | jq -r 'select(.verify | not) | .folder // .reason // "unmapped"' | paste -sd, - 2>/dev/null || true)"
118
+ if [ -n "$_unverified" ]; then
119
+ _add verify unrun "no verify script for: $_unverified — an unrun gate is not a pass"
120
+ fi
121
+ fi
122
+
123
+ # ---- 2. objective security gate (zero-LLM backstop) ----
124
+ if command -v gitleaks >/dev/null 2>&1; then
125
+ _run secrets gitleaks detect --no-git --source "$ROOT" --redact -q
126
+ else
127
+ _add secrets unrun "gitleaks not installed — absent tool is an unrun check, never green"
128
+ fi
129
+ if printf '%s\n' "$files_txt" | grep -qE '(bun\.lock|package\.json)$'; then
130
+ _run dep-audit bash -c "cd '$ROOT' && bun audit --audit-level moderate"
131
+ else
132
+ _add dep-audit n/a "no dependency file in the diff"
133
+ fi
134
+
135
+ # security greps over the diff (w4-tools.md § Security grep) — hits are a FAIL
136
+ local sec
137
+ sec="$(git -C "$ROOT" diff HEAD 2>/dev/null | grep -E '^\+' \
138
+ | grep -vE '^\+\+\+' \
139
+ | grep -E "eval\(|dangerouslySetInnerHTML|Access-Control-Allow-Origin.*\*" \
140
+ | grep -vE '// allow|sanitize|DOMPurify' | head -5 || true)"
141
+ if [ -n "$sec" ]; then _add security-grep fail "$(printf '%s' "$sec" | tr '\n' ' ')"
142
+ else _add security-grep pass "0 hits over the diff"; fi
143
+
144
+ # ---- 3. surface checklist (missing file = W4 FAIL, by do.md rule) ----
145
+ if [ ! -f "$ROOT/.w2-surface-checklist.json" ]; then
146
+ _add surface-checklist fail ".w2-surface-checklist.json missing — W2 must declare surfaces or surface:none+reason"
147
+ elif [ "$(jq -r '.surface // empty' "$ROOT/.w2-surface-checklist.json")" = "none" ]; then
148
+ if [ -n "$(jq -r '.reason // empty' "$ROOT/.w2-surface-checklist.json")" ]; then
149
+ _add surface-checklist pass "surface:none with a reason"
150
+ else
151
+ _add surface-checklist fail "surface:none without a reason"
152
+ fi
153
+ else
154
+ _add surface-checklist pass "surfaces declared"
155
+ fi
156
+
157
+ # ---- 3b. per-surface WIRING greps (do.md W4: nav_entry / inbound_links /
158
+ # sdk_export / mcp_tool / cli_verb). These are NOT the same check as
159
+ # `do-reconcile.sh navigation|sdk`; each declared surface must be found
160
+ # wired at its named site. Needs a folder to resolve src/ paths against.
161
+ local cl="$ROOT/.w2-surface-checklist.json" wroot wfails=0 wchecked=0 route entry page txt ex tool verb
162
+ if [ -f "$cl" ] && [ "$(jq -r '.surface // empty' "$cl")" != "none" ]; then
163
+ wroot="$(printf '%s\n' "$folders" | jq -r 'select(.verify) | .folder' | head -1)"
164
+ if [ -z "$wroot" ]; then
165
+ _add surface-wiring unrun "no code folder resolved — cannot locate src/ to grep the declared surfaces"
166
+ else
167
+ # KNOWN, MEASURED 2026-09-01: this is do.md's grep VERBATIM, and
168
+ # one.ie/web/src/lib/navigation.ts contains exactly two `path:` lines, both
169
+ # of them TypeScript parameter declarations — no route table. So a cycle
170
+ # declaring nav_entry fails here. That is not a bug introduced by moving
171
+ # the check into a script: do.md's own inline `grep -q … || exit 1` fails
172
+ # identically. Transcribed faithfully on purpose; fixing the check is a
173
+ # separate change to the contract, not something a runner may do quietly.
174
+ while IFS= read -r route; do
175
+ [ -n "$route" ] || continue; wchecked=$((wchecked+1))
176
+ grep -q "path:.*$route" "$ROOT/$wroot/src/lib/navigation.ts" 2>/dev/null \
177
+ || { wfails=$((wfails+1)); }
178
+ done < <(jq -r '.surfaces[]? | select(.nav_entry) | .nav_entry' "$cl" 2>/dev/null)
179
+ while IFS= read -r entry; do
180
+ [ -n "$entry" ] || continue; wchecked=$((wchecked+1))
181
+ page="${entry%%:*}"; txt="${entry#*:}"
182
+ grep -q "$txt" "$ROOT/$wroot/src/pages/$page.astro" 2>/dev/null || wfails=$((wfails+1))
183
+ done < <(jq -r '.surfaces[]? | select(.inbound_links) | .inbound_links[]' "$cl" 2>/dev/null)
184
+ while IFS= read -r ex; do
185
+ [ -n "$ex" ] || continue; wchecked=$((wchecked+1))
186
+ grep -q "export.*from.*$ex" "$ROOT/packages/sdk/src/index.ts" 2>/dev/null || wfails=$((wfails+1))
187
+ done < <(jq -r '.surfaces[]? | select(.sdk_export) | .sdk_export' "$cl" 2>/dev/null)
188
+ while IFS= read -r tool; do
189
+ [ -n "$tool" ] || continue; wchecked=$((wchecked+1))
190
+ grep -rq "$tool" "$ROOT/packages/mcp/src/index.ts" 2>/dev/null || wfails=$((wfails+1))
191
+ done < <(jq -r '.surfaces[]? | select(.mcp_tool) | .mcp_tool' "$cl" 2>/dev/null)
192
+ while IFS= read -r verb; do
193
+ [ -n "$verb" ] || continue; wchecked=$((wchecked+1))
194
+ grep -rq "$verb" "$ROOT/packages/cli/src/index.ts" 2>/dev/null || wfails=$((wfails+1))
195
+ done < <(jq -r '.surfaces[]? | select(.cli_verb) | .cli_verb' "$cl" 2>/dev/null)
196
+ if [ "$wfails" -gt 0 ]; then
197
+ _add surface-wiring fail "$wfails of $wchecked declared surfaces are NOT wired at their named site"
198
+ else
199
+ _add surface-wiring pass "$wchecked declared surfaces wired"
200
+ fi
201
+ fi
202
+ else
203
+ _add surface-wiring n/a "no surfaces declared (surface:none, or no checklist — see surface-checklist)"
204
+ fi
205
+
206
+ # ---- 3c. the GOAL GATE and the PROMISE ORACLE.
207
+ # do.md's W4 block runs `$(cycle.demo.command)` and, when text/<slug>.md
208
+ # exists, `do-prove.sh <slug>`. This script cannot invent either: the demo
209
+ # command lives in the cycle, and the slug in the plan. They are therefore
210
+ # reported UNRUN unless supplied — which, by the four-state law, keeps
211
+ # deterministic_pass RED. A caller who runs the script bare CANNOT get a
212
+ # green without having run the goal gate. That is the point.
213
+ if [ -n "$DEMO_CMD" ]; then
214
+ _run demo-gate bash -c "cd '$ROOT' && $DEMO_CMD"
215
+ else
216
+ _add demo-gate unrun "no --demo <cmd> supplied — the cycle's goal gate has NOT run"
217
+ fi
218
+ if [ -n "$SLUG" ]; then
219
+ if [ -f "$ROOT/text/$SLUG.md" ]; then
220
+ _run promise-prove bash "$S/do-prove.sh" "$SLUG"
221
+ else
222
+ _add promise-prove n/a "no text/$SLUG.md — no promise to prove"
223
+ fi
224
+ else
225
+ _add promise-prove unrun "no --slug supplied — do-prove.sh has NOT run"
226
+ fi
227
+
228
+ # ---- 4. the RECONCILES gate: the 7 canons ----
229
+ # $files_txt is deliberately UNQUOTED here so newline-separated paths become
230
+ # argv. Repo paths carry no spaces; a path that did would need bash 4 arrays.
231
+ local canon
232
+ if [ "${W4_SKIP_RECONCILE:-0}" = "1" ]; then
233
+ # ESCAPE HATCH FOR THE SELF-TEST ONLY. `do-reconcile.sh types` is a tsc that
234
+ # can queue for minutes behind the governor, and the self-test calls this
235
+ # script six times to prove the cheap gates flip. Skipping is safe ONLY
236
+ # because a skip is `unrun`, not `pass` — the verdict still goes red, and
237
+ # test 6 below asserts exactly that. Never set this in a real cycle.
238
+ for canon in types dictionary substrate sdk authority design navigation; do
239
+ _add "reconcile:$canon" unrun "W4_SKIP_RECONCILE=1 — an unrun canon is not a passed canon"
240
+ done
241
+ _add consumer-sweep unrun "W4_SKIP_RECONCILE=1 — an unrun sweep is not a passed sweep"
242
+ else
243
+ _run reconcile:types bash "$S/do-reconcile.sh" types
244
+ _run reconcile:dictionary bash "$S/do-reconcile.sh" dictionary $files_txt
245
+ # do.md scopes these: `do-reconcile.sh navigation <surface>` / `design <file>`
246
+ # are SURFACE-task canons. A markdown doc is not a UI surface, so feeding
247
+ # text/*.md to `navigation` produces a guaranteed false RED ("text/do.md not
248
+ # registered in menu.ts" — measured 2026-09-01 on this very change). Excluded
249
+ # here to restore do.md's own scoping; everything that IS a surface still runs.
250
+ local surface_txt
251
+ surface_txt="$(printf '%s\n' "$files_txt" | grep -vE '\.md$' || true)"
252
+ for canon in substrate sdk authority; do
253
+ _run "reconcile:$canon" bash "$S/do-reconcile.sh" "$canon" $files_txt
254
+ done
255
+ for canon in design navigation; do
256
+ if [ -n "$surface_txt" ]; then
257
+ _run "reconcile:$canon" bash "$S/do-reconcile.sh" "$canon" $surface_txt
258
+ else
259
+ _add "reconcile:$canon" n/a "no non-markdown surface in the diff"
260
+ fi
261
+ done
262
+
263
+ # ---- 5. consumer sweep ----
264
+ if [ "$nfiles" -gt 0 ]; then
265
+ _run consumer-sweep bash "$S/do-consumer-sweep.sh" --exec $files_txt
266
+ else
267
+ _add consumer-sweep n/a "empty diff"
268
+ fi
269
+ fi
270
+
271
+ # ---- 6. tsc delta vs the W0 baseline (hard gate: <= 0) ----
272
+ if [ -f "$ROOT/.w0-baseline.json" ]; then
273
+ local base now
274
+ base="$(jq -r '.tscErrors // empty' "$ROOT/.w0-baseline.json" 2>/dev/null || echo '')"
275
+ if [ -z "$base" ]; then
276
+ _add tsc-delta unrun ".w0-baseline.json has no tscErrors"
277
+ else
278
+ set +e; now="$(cd "$ROOT" && "${_GATE[@]}" bunx tsc --noEmit 2>&1 | grep -c 'error TS')"; set -e
279
+ if [ "$now" -le "$base" ]; then _add tsc-delta pass "baseline=$base now=$now"
280
+ else _add tsc-delta fail "baseline=$base now=$now — new type errors"; fi
281
+ fi
282
+ else
283
+ _add tsc-delta unrun "no .w0-baseline.json — W0 did not run, so the delta is unknown"
284
+ fi
285
+
286
+ # ---- verdict ----
287
+ local gates fails unruns
288
+ gates="$(jq -s '.' "$RESULTS_FILE")"
289
+ fails="$(printf '%s' "$gates" | jq '[.[] | select(.status=="fail")] | length')"
290
+ unruns="$(printf '%s' "$gates" | jq '[.[] | select(.status=="unrun")] | length')"
291
+ rm -f "$RESULTS_FILE"
292
+
293
+ jq -n --argjson gates "$gates" --argjson fails "$fails" --argjson unruns "$unruns" \
294
+ --argjson files "$(printf '%s\n' "$files_txt" | jq -R -s 'split("\n") | map(select(length>0))')" \
295
+ '{files:$files, gates:$gates, fails:$fails, unrun:$unruns,
296
+ deterministic_pass: ($fails == 0 and $unruns == 0),
297
+ note: "deterministic_pass requires ZERO fails AND ZERO unruns. An unrun gate is not a passed gate. The rubric verdict (goal-fit, taste) is scored on top of this pack; it may only LOWER the outcome, never raise a fail to a pass."}'
298
+ }
299
+
300
+ _self_test() {
301
+ local fails=0 out t
302
+ t="$(mktemp -d)"; trap 'rm -rf "$t"' RETURN
303
+
304
+ # 1. shape: a run emits valid JSON with a gate list and a verdict.
305
+ # Use a CODE file, not a doc: a doc-only diff makes the verify lane `n/a`
306
+ # by rule, which would hide the unrun branch test 2 exists to prove.
307
+ out="$(W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" --files one.ie/web/src/lib/authority.ts 2>/dev/null)" || true
308
+ if printf '%s' "$out" | jq -e '(.gates|length) > 5 and has("deterministic_pass")' >/dev/null 2>&1; then
309
+ echo "ok: evidence pack is valid JSON with a gate list and a verdict"
310
+ else
311
+ echo "FAIL: evidence pack malformed"; fails=$((fails+1))
312
+ fi
313
+
314
+ # 2. RED PROOF (a) — an UNRUN gate must not read as a pass.
315
+ # W4_SKIP_VERIFY=1 forces the verify lane unrun; deterministic_pass MUST be false.
316
+ if printf '%s' "$out" | jq -e '[.gates[]|select(.gate=="verify" and .status=="unrun")]|length==1' >/dev/null 2>&1; then
317
+ if printf '%s' "$out" | jq -e '.deterministic_pass == false' >/dev/null 2>&1; then
318
+ echo "ok: RED PROOF — a skipped verify is reported unrun and the verdict goes RED"
319
+ else
320
+ echo "FAIL: RED PROOF — unrun verify still produced deterministic_pass:true"; fails=$((fails+1))
321
+ fi
322
+ else
323
+ echo "FAIL: RED PROOF — W4_SKIP_VERIFY did not surface as an unrun verify gate"; fails=$((fails+1))
324
+ fi
325
+
326
+ # 3/4. RED PROOF (b) — the surface-checklist gate must move BOTH ways.
327
+ # A gate that is always red proves as little as one that is always green,
328
+ # so drive it green -> red -> green over a real file and assert each flip.
329
+ local cl="$ROOT/.w2-surface-checklist.json" had_cl=false
330
+ if [ -f "$cl" ]; then had_cl=true; cp "$cl" "$t/cl.bak"; fi
331
+ _cl_status() {
332
+ W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" --files text/do.md 2>/dev/null \
333
+ | jq -r '[.gates[]|select(.gate=="surface-checklist")][0].status'
334
+ }
335
+
336
+ rm -f "$cl"
337
+ if [ "$(_cl_status)" = "fail" ]; then
338
+ echo "ok: RED PROOF — an ABSENT .w2-surface-checklist.json fails the cycle"
339
+ else
340
+ echo "FAIL: RED PROOF — absent surface checklist did not fail"; fails=$((fails+1))
341
+ fi
342
+
343
+ printf '{"surface":"none"}\n' > "$cl"
344
+ if [ "$(_cl_status)" = "fail" ]; then
345
+ echo "ok: RED PROOF — surface:none WITHOUT a reason fails"
346
+ else
347
+ echo "FAIL: RED PROOF — surface:none without a reason passed"; fails=$((fails+1))
348
+ fi
349
+
350
+ # surface-WIRING, both directions. This is the gate that was MISSING from the
351
+ # first cut, so it gets the same treatment as the rest: prove it red, prove it
352
+ # green. `sdk_export` is used because its target (packages/sdk/src/index.ts) is
353
+ # a fixed path, unlike nav/inbound which resolve against the cycle's folder.
354
+ # A CODE file is passed so do-folder resolves $wroot (a doc-only diff cannot).
355
+ _wire_status() {
356
+ W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" \
357
+ --files one.ie/web/src/lib/authority.ts 2>/dev/null \
358
+ | jq -r '[.gates[]|select(.gate=="surface-wiring")][0].status'
359
+ }
360
+ printf '{"surfaces":[{"sdk_export":"zzz-not-a-real-export"}]}\n' > "$cl"
361
+ if [ "$(_wire_status)" = "fail" ]; then
362
+ echo "ok: RED PROOF — a declared surface that is NOT wired fails surface-wiring"
363
+ else
364
+ echo "FAIL: RED PROOF — an unwired declared surface did not fail"; fails=$((fails+1))
365
+ fi
366
+ printf '{"surfaces":[{"sdk_export":"types"}]}\n' > "$cl"
367
+ if [ "$(_wire_status)" = "pass" ]; then
368
+ echo "ok: GREEN PROOF — a genuinely wired surface passes (surface-wiring is not stuck red)"
369
+ else
370
+ echo "FAIL: GREEN PROOF — a wired surface did not pass surface-wiring"; fails=$((fails+1))
371
+ fi
372
+
373
+ printf '{"surface":"none","reason":"self-test fixture"}\n' > "$cl"
374
+ if [ "$(_cl_status)" = "pass" ]; then
375
+ echo "ok: GREEN PROOF — surface:none WITH a reason passes (the gate is not stuck red)"
376
+ else
377
+ echo "FAIL: GREEN PROOF — a well-formed checklist did not pass"; fails=$((fails+1))
378
+ fi
379
+
380
+ rm -f "$cl"
381
+ if [ "$had_cl" = true ]; then cp "$t/cl.bak" "$cl"; fi
382
+
383
+ # 5. RED PROOF — the GOAL GATE. Bare, it must be `unrun`; supplied and passing,
384
+ # it must flip to `pass`. A caller can never get a green without running it.
385
+ local dg
386
+ dg="$(W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" --files text/do.md 2>/dev/null \
387
+ | jq -r '[.gates[]|select(.gate=="demo-gate")][0].status')"
388
+ if [ "$dg" = "unrun" ]; then
389
+ echo "ok: RED PROOF — no --demo means the goal gate is UNRUN (verdict cannot be green)"
390
+ else
391
+ echo "FAIL: RED PROOF — bare run reported demo-gate as '$dg', not unrun"; fails=$((fails+1))
392
+ fi
393
+ dg="$(W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" --demo true --files text/do.md 2>/dev/null \
394
+ | jq -r '[.gates[]|select(.gate=="demo-gate")][0].status')"
395
+ if [ "$dg" = "pass" ]; then
396
+ echo "ok: GREEN PROOF — a supplied, passing --demo flips the goal gate to pass"
397
+ else
398
+ echo "FAIL: GREEN PROOF — --demo true reported '$dg'"; fails=$((fails+1))
399
+ fi
400
+ dg="$(W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" --demo false --files text/do.md 2>/dev/null \
401
+ | jq -r '[.gates[]|select(.gate=="demo-gate")][0].status')"
402
+ if [ "$dg" = "fail" ]; then
403
+ echo "ok: RED PROOF — a FAILING --demo fails the goal gate"
404
+ else
405
+ echo "FAIL: RED PROOF — --demo false reported '$dg'"; fails=$((fails+1))
406
+ fi
407
+
408
+ # 6. The PROMISE ORACLE's branch selection — proven without running do-prove.sh
409
+ # (a real prove is a browser run; what needs proving here is the routing).
410
+ local pp
411
+ pp="$(W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" --files text/do.md 2>/dev/null \
412
+ | jq -r '[.gates[]|select(.gate=="promise-prove")][0].status')"
413
+ [ "$pp" = "unrun" ] \
414
+ && echo "ok: RED PROOF — no --slug means the promise oracle is UNRUN" \
415
+ || { echo "FAIL: no --slug reported promise-prove as '$pp', not unrun"; fails=$((fails+1)); }
416
+ pp="$(W4_SKIP_VERIFY=1 W4_SKIP_RECONCILE=1 bash "$S/do-w4-gates.sh" --slug __nosuchpromise__ --files text/do.md 2>/dev/null \
417
+ | jq -r '[.gates[]|select(.gate=="promise-prove")][0].status')"
418
+ [ "$pp" = "n/a" ] \
419
+ && echo "ok: a slug with no text/<slug>.md is n/a — there is no promise to prove" \
420
+ || { echo "FAIL: absent promise reported '$pp', not n/a"; fails=$((fails+1)); }
421
+
422
+ # 7. RED PROOF — the self-test's OWN escape hatch must not read as a pass.
423
+ # Tests 1-5 run with W4_SKIP_RECONCILE=1. If that skip scored `pass`, every
424
+ # assertion above would be measured against a gutted battery.
425
+ if printf '%s' "$out" | jq -e '[.gates[]|select(.gate|startswith("reconcile:"))|select(.status!="unrun")]|length==0' >/dev/null 2>&1; then
426
+ echo "ok: RED PROOF — the self-test's own W4_SKIP_RECONCILE reads as unrun, not pass"
427
+ else
428
+ echo "FAIL: RED PROOF — a skipped canon scored something other than unrun"; fails=$((fails+1))
429
+ fi
430
+
431
+ if [ "$fails" -eq 0 ]; then echo "do-w4-gates: self-test PASS"; return 0; fi
432
+ echo "do-w4-gates: self-test FAIL ($fails)"; return 1
433
+ }
434
+
435
+ [ "${1:-}" = "--self-test" ] && { _self_test; exit $?; }
436
+
437
+ PATHS=""
438
+ while [ "$#" -gt 0 ]; do
439
+ case "$1" in
440
+ --demo) DEMO_CMD="${2:-}"; shift 2 ;;
441
+ --slug) SLUG="${2:-}"; shift 2 ;;
442
+ --files) shift ;;
443
+ -h|--help)
444
+ echo "usage: do-w4-gates.sh [--files <path>...] [--demo '<cmd>'] [--slug <slug>] | --self-test" >&2
445
+ exit 2 ;;
446
+ *) PATHS="$PATHS $1"; shift ;;
447
+ esac
448
+ done
449
+
450
+ # unquoted on purpose — see the RECONCILES note in _gates
451
+ _gates $PATHS
@@ -286,9 +286,20 @@ for n in $ids; do
286
286
  [ "$code" != "$es" ] && { ok=0; why="http=$code (want $es)"; }
287
287
  fi
288
288
  if [ "$ok" -eq 1 ] && [ -n "$et" ]; then
289
- if ! curl -s --max-time 20 "${cookie_args[@]+"${cookie_args[@]}"}" "$url" 2>/dev/null | grep -qiF "$et"; then
289
+ # Body FIRST, then grep it never `curl | grep -q` here. `grep -q` exits
290
+ # the moment it matches, curl is still writing, curl dies of SIGPIPE (141),
291
+ # and `set -o pipefail` (line 73) hands that 141 to the `if`. The stop then
292
+ # fails *because the text was found*, on any page big enough that curl has
293
+ # not finished writing. Measured 2026-08-04: `/u/one/shop` serves "Shop"
294
+ # twice and the stop still reported "text 'Shop' not in served HTML",
295
+ # reproducibly. A false RED — the mirror of the false GREEN do-prove had.
296
+ # A here-string, not a pipe: `printf … | grep -q` reintroduces the exact
297
+ # same SIGPIPE race one process further left.
298
+ body="$(curl -s --max-time 20 "${cookie_args[@]+"${cookie_args[@]}"}" "$url" 2>/dev/null || true)"
299
+ if ! grep -qiF -- "$et" <<<"$body"; then
290
300
  ok=0; why="text '$et' not in served HTML"
291
301
  fi
302
+ unset body
292
303
  fi
293
304
  fi
294
305
  if [ "$ok" -eq 1 ] && [ -n "$ec" ]; then