@oneie/claude 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/abm-strategist.md +67 -1
  3. package/agents/ads-meta.md +67 -1
  4. package/agents/analyst.md +67 -1
  5. package/agents/animator.md +108 -0
  6. package/agents/architect.md +269 -20
  7. package/agents/brand-guardian.md +67 -1
  8. package/agents/brand-strategist.md +67 -1
  9. package/agents/campaign-content.md +67 -1
  10. package/agents/campaign-email.md +67 -1
  11. package/agents/campaign-sms.md +67 -1
  12. package/agents/campaign-social.md +67 -1
  13. package/agents/cco.md +83 -2
  14. package/agents/ceo.md +108 -11
  15. package/agents/chairman.md +197 -0
  16. package/agents/cmo.md +82 -2
  17. package/agents/community-greeter.md +67 -1
  18. package/agents/community-moderator.md +67 -1
  19. package/agents/compliance.md +67 -1
  20. package/agents/copywriter.md +67 -1
  21. package/agents/creative-strategist.md +67 -1
  22. package/agents/cro.md +81 -1
  23. package/agents/cto.md +266 -28
  24. package/agents/customer-interviewer.md +67 -1
  25. package/agents/customer-researcher.md +67 -1
  26. package/agents/customer-success-manager.md +67 -1
  27. package/agents/customer-trainer.md +67 -1
  28. package/agents/cxo.md +82 -1
  29. package/agents/demand-creator.md +67 -1
  30. package/agents/demo-mover.md +67 -1
  31. package/agents/demo-specialist.md +67 -1
  32. package/agents/demo-thai-family-law.md +67 -1
  33. package/agents/designer.md +67 -1
  34. package/agents/discovery-caller.md +67 -1
  35. package/agents/doctor.md +269 -0
  36. package/agents/educate-coach.md +67 -1
  37. package/agents/elevate-tutor.md +67 -1
  38. package/agents/email-lifecycle-marketer.md +67 -1
  39. package/agents/engage-specialist.md +67 -1
  40. package/agents/events-coordinator.md +67 -1
  41. package/agents/foundation-builder.md +67 -1
  42. package/agents/funnel-architect.md +67 -1
  43. package/agents/gift-creator.md +67 -1
  44. package/agents/google-ads.md +67 -1
  45. package/agents/guide.md +67 -1
  46. package/agents/helpdesk-dispatcher.md +67 -1
  47. package/agents/hook-specialist.md +67 -1
  48. package/agents/identify-optimizer.md +67 -1
  49. package/agents/implementer.md +313 -45
  50. package/agents/incident-commander.md +67 -1
  51. package/agents/insights-lead.md +87 -1
  52. package/agents/journey-runner.md +67 -1
  53. package/agents/linkedin-ads.md +67 -1
  54. package/agents/live-sales-chat.md +67 -1
  55. package/agents/market-researcher.md +67 -1
  56. package/agents/media-buyer.md +67 -1
  57. package/agents/memory-keeper.md +195 -0
  58. package/agents/movers-customer-researcher.md +67 -1
  59. package/agents/movers-foundation-builder.md +67 -1
  60. package/agents/movers-market-researcher.md +67 -1
  61. package/agents/movers-pricing-strategist.md +67 -1
  62. package/agents/nurture-architect.md +67 -1
  63. package/agents/offer-architect.md +67 -1
  64. package/agents/onboarder.md +67 -1
  65. package/agents/onboarding-specialist.md +67 -1
  66. package/agents/operations-dashboard.md +87 -1
  67. package/agents/perf-engineer.md +333 -37
  68. package/agents/playbook-writer.md +67 -1
  69. package/agents/plg-strategist.md +67 -1
  70. package/agents/positioning-architect.md +67 -1
  71. package/agents/press-officer.md +67 -1
  72. package/agents/pricing-strategist.md +67 -1
  73. package/agents/privacy-officer.md +67 -1
  74. package/agents/referral-manager.md +67 -1
  75. package/agents/refine-analyst.md +67 -1
  76. package/agents/release-manager.md +446 -39
  77. package/agents/renewals-upsell-rep.md +67 -1
  78. package/agents/review-engineer.md +319 -45
  79. package/agents/rewards-steward.md +67 -1
  80. package/agents/sales-call-coach.md +67 -1
  81. package/agents/sales-closer.md +67 -1
  82. package/agents/security-auditor.md +343 -48
  83. package/agents/sell-closer.md +67 -1
  84. package/agents/share-amplifier.md +67 -1
  85. package/agents/social-media-manager.md +67 -1
  86. package/agents/storyteller.md +301 -0
  87. package/agents/strategist.md +67 -1
  88. package/agents/strategy-aligner.md +67 -1
  89. package/agents/support-agent.md +67 -1
  90. package/agents/tagger.md +327 -0
  91. package/agents/tech-writer.md +195 -22
  92. package/agents/test-engineer.md +398 -29
  93. package/agents/tiktok-ads.md +67 -1
  94. package/agents/tracking-engineer.md +67 -1
  95. package/agents/trailkeeper.md +181 -0
  96. package/agents/upsell-strategist.md +67 -1
  97. package/agents/voice.md +67 -1
  98. package/agents/w1-recon.md +1 -1
  99. package/agents/w2-decide.md +1 -1
  100. package/agents/w3-edit.md +8 -2
  101. package/agents/w4-verify.md +13 -0
  102. package/agents/workflow-optimiser.md +81 -1
  103. package/commands/close.md +916 -160
  104. package/commands/deploy.md +102 -724
  105. package/commands/do.md +58 -2
  106. package/commands/sweep.md +159 -0
  107. package/commands/tasks.md +222 -0
  108. package/hooks/scripts/dev-only.sh +135 -0
  109. package/hooks/scripts/git-add-guard.sh +37 -2
  110. package/hooks/scripts/session-start.sh +32 -4
  111. package/package.json +1 -1
  112. package/rules/scripts.md +85 -0
  113. package/scripts/CLAUDE.md +315 -0
  114. package/scripts/ad-copy-lint.sh +656 -0
  115. package/scripts/agent-actor-parity.sh +129 -0
  116. package/scripts/blocks-manifest-cached.sh +100 -0
  117. package/scripts/chat-context-check.sh +89 -0
  118. package/scripts/chrome.mjs +18 -0
  119. package/scripts/close-metrics.sh +587 -0
  120. package/scripts/close-owner.sh +326 -0
  121. package/scripts/db-sync-lock-check.sh +116 -0
  122. package/scripts/deploy-emit.sh +311 -0
  123. package/scripts/deploy-gate-check.sh +155 -0
  124. package/scripts/deploy-ready.sh +78 -0
  125. package/scripts/deploy-record.sh +605 -0
  126. package/scripts/deploy-schema-check.sh +58 -0
  127. package/scripts/deploy.sh +393 -243
  128. package/scripts/do-auto.sh +127 -26
  129. package/scripts/do-board.sh +429 -0
  130. package/scripts/do-close.sh +1184 -0
  131. package/scripts/do-consumer-sweep.sh +18 -1
  132. package/scripts/do-decide.sh +476 -0
  133. package/scripts/do-fleet.sh +8 -2
  134. package/scripts/do-plan-json.mjs +110 -12
  135. package/scripts/do-prove-selftest.sh +108 -0
  136. package/scripts/do-prove.sh +86 -10
  137. package/scripts/do-rank.py +200 -3
  138. package/scripts/do-reconcile.sh +73 -12
  139. package/scripts/do-signal.sh +101 -23
  140. package/scripts/do-smoke.sh +18 -1
  141. package/scripts/do-w4-gates.sh +11 -1
  142. package/scripts/do-world-check.sh +153 -0
  143. package/scripts/download-stats.sh +172 -0
  144. package/scripts/factory-brief-check.sh +330 -0
  145. package/scripts/factory-check.sh +18 -1
  146. package/scripts/factory-close-check.sh +257 -0
  147. package/scripts/factory-emit.sh +211 -0
  148. package/scripts/factory-executor-check.mjs +353 -0
  149. package/scripts/factory-peak.sh +301 -0
  150. package/scripts/factory-repo.sh +71 -0
  151. package/scripts/factory-review-check.mjs +61 -0
  152. package/scripts/factory-tasks-check.sh +18 -1
  153. package/scripts/fixtures/factory-brief-real.md +44 -0
  154. package/scripts/flywheel-outcome.sh +63 -0
  155. package/scripts/gate-reaper-check.sh +98 -0
  156. package/scripts/gate-reaper.sh +9 -0
  157. package/scripts/gate-watchdog.sh +619 -0
  158. package/scripts/gc-content-check.sh +142 -0
  159. package/scripts/gh-traffic-capture.sh +153 -0
  160. package/scripts/govern-order-check.sh +202 -0
  161. package/scripts/governor-doors-check.sh +86 -5
  162. package/scripts/health.sh +448 -0
  163. package/scripts/id-inventory.mjs +418 -0
  164. package/scripts/incident.sh +212 -0
  165. package/scripts/land.sh +755 -45
  166. package/scripts/lib/gc-finished.sh +77 -0
  167. package/scripts/livekit-ratchet.sh +18 -1
  168. package/scripts/machine-check.sh +1 -1
  169. package/scripts/memory-index-budget.sh +79 -0
  170. package/scripts/npm-downloads.sh +109 -0
  171. package/scripts/one-agents.mjs +204 -8
  172. package/scripts/one-resume.sh +31 -3
  173. package/scripts/pr-body.sh +335 -0
  174. package/scripts/preview-fd-check.sh +289 -0
  175. package/scripts/redirect-lint.sh +169 -0
  176. package/scripts/release.sh +40 -6
  177. package/scripts/resume-lost-sessions.sh +68 -0
  178. package/scripts/shoot-pages.mjs +140 -0
  179. package/scripts/signal-meta-backfill.ts +451 -0
  180. package/scripts/signal-watch.sh +63 -6
  181. package/scripts/speed-cache-check.sh +12 -2
  182. package/scripts/sweep.sh +426 -0
  183. package/scripts/task-titles-dump.ts +101 -0
  184. package/scripts/test-cached.sh +47 -10
  185. package/scripts/test-lanes.sh +14 -0
  186. package/scripts/thread-name-backfill.ts +215 -0
  187. package/scripts/triage-shape-check.sh +149 -0
  188. package/scripts/tsc-cached.sh +155 -8
  189. package/scripts/typedb-flake-check.sh +3 -1
  190. package/scripts/urls-lint.sh +8 -0
  191. package/scripts/verify-board-doors.sh +80 -0
  192. package/scripts/verify-fast.sh +159 -6
  193. package/scripts/worktree-up.sh +21 -3
  194. package/skills/astro/SKILL.md +9 -3
  195. package/skills/astro/optimize-performance.md +3 -2
  196. package/skills/cloudflare/SKILL.md +3 -2
  197. package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
  198. package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
  199. package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
  200. package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  201. package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  202. package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  203. package/skills/cloudflare-security-audit/HUNTING.md +251 -0
  204. package/skills/cloudflare-security-audit/LICENSE +21 -0
  205. package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  206. package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  207. package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
  208. package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
  209. package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  210. package/skills/cloudflare-security-audit/SKILL.md +192 -0
  211. package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  212. package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  213. package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  214. package/skills/cloudflare-security-audit/report-schema.json +461 -0
  215. package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
  216. package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
  217. package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
  218. package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
  219. package/skills/deploy/REFERENCE.md +713 -0
  220. package/skills/deploy/SKILL.md +140 -0
  221. package/skills/fleet-audit/SKILL.md +58 -0
  222. package/skills/meeting/SKILL.md +220 -0
  223. package/skills/planning/SKILL.md +256 -0
  224. package/skills/shadcn/SKILL.md +1 -1
  225. package/skills/typedb/SKILL.md +7 -0
  226. package/skills/voice/SKILL.md +94 -6
  227. package/skills/voice/corpus-check.sh +87 -0
  228. package/templates/template-agent.md +7 -1
  229. package/templates/template-feature.md +9 -0
  230. package/templates/template-todo.md +29 -0
@@ -49,25 +49,83 @@ const fm = t.match(/^---\n([\s\S]*?)\n---/)
49
49
  if (!fm) fail('no frontmatter')
50
50
  const F = fm[1]
51
51
 
52
- // batches: `- [C0, C2]` lines under `batches:`
53
- const bm = F.match(/^batches:\n((?:[ \t]+- \[.*\]\n?)+)/m)
54
- const batches = bm
55
- ? bm[1].trim().split('\n').map((l) =>
56
- l.replace(/^[\s-]*\[|\]\s*$/g, '').split(',').map((s) => s.trim()).filter(Boolean))
57
- : []
52
+ // batches: `- [C0, C2]` rows under `batches:`
53
+ //
54
+ // DEFECT this shape exists to prevent (measured 2026-09-08 on text/ads-loop-todo.md):
55
+ // the capture used to be /^batches:\n((?:[ \t]+- \[.*\]\n?)+)/m. A trailing
56
+ // `# comment` after the closing `]` is legal YAML and carried by most plans in
57
+ // text/ — it leaves text after `]`, so `\n?` matched empty, the `+` quantifier
58
+ // stopped at that row, and EVERY batch from there on was dropped. Nothing
59
+ // called fail(): the script still exited 0 and still emitted a well-formed
60
+ // object. ads-loop parsed as [["C1"…"C9"]] instead of [["C1"…"C9"],["C10"]],
61
+ // so /do would have run nine cycles of a ten-cycle plan and reported complete.
62
+ //
63
+ // Two changes, and the second is the load-bearing one:
64
+ // 1. a row may carry a trailing comment and trailing whitespace;
65
+ // 2. the block is delimited by INDENTATION (YAML's own rule) and every line
66
+ // inside it must parse — a line that does not is a REFUSAL, never a
67
+ // shorter list. Comment-tolerance alone would leave the next unreadable
68
+ // row failing in exactly the same silent way.
69
+ // Blank lines and whole-line `#` comments inside the block are legal and skipped.
70
+ // Inline flow style (`batches: [[C0]]`) still yields [] and fails at the
71
+ // `no batches:` invariant below, exactly as before.
72
+ const BATCH_ROW = /^[ \t]+-[ \t]*\[([^\]]*)\][ \t]*(?:#.*)?$/
73
+ const fLines = F.split('\n')
74
+ const bStart = fLines.findIndex((l) => /^batches:[ \t]*$/.test(l))
75
+ let batches = []
76
+ if (bStart >= 0) {
77
+ const rows = []
78
+ const bad = []
79
+ for (let i = bStart + 1; i < fLines.length; i++) {
80
+ const line = fLines[i]
81
+ if (!line.trim()) continue // blank line inside the block
82
+ if (!/^[ \t]/.test(line)) break // a column-0 line is the next key: block over
83
+ if (/^[ \t]*#/.test(line)) continue // whole-line comment
84
+ const m = line.match(BATCH_ROW)
85
+ if (m) rows.push(m[1])
86
+ else bad.push(line)
87
+ }
88
+ if (bad.length) {
89
+ fail(
90
+ `\`batches:\` carries ${bad.length} row(s) this parser cannot read. ` +
91
+ 'Refusing: a truncated batch list must never read as a complete one. ' +
92
+ `A row is \`- [C1, C2]\` with an optional trailing # comment. First offender: ${JSON.stringify(bad[0])}`,
93
+ )
94
+ }
95
+ batches = rows.map((r) => r.split(',').map((s) => s.trim()).filter(Boolean))
96
+ }
58
97
 
59
98
  // outcome: the kill-switch. A YAML double-quoted scalar carries \" and \\ escapes;
60
99
  // stripping the outer quotes WITHOUT unescaping leaves a stray backslash that
61
100
  // JSON.stringify then doubles, emitting invalid JSON (measured: lifecycle-money,
62
101
  // lifecycle-human, both of whose outcomes contain a quoted grep pattern).
102
+ // A YAML scalar may carry a trailing `# comment`, and text/template-todo.md puts one
103
+ // on EVERY key a plan copies — `anchor: "" # filled by do-board.sh`,
104
+ // `board_workspace: one # where the rows live…`. The old body tested
105
+ // `endsWith('"')`, which a commented value fails, so it returned the raw line
106
+ // INCLUDING the comment. Measured 2026-09-12: boardAnchor came back as
107
+ // `"task:01a0…" # filled by do-board.sh` and do-board handed that to
108
+ // tasks:subtask as `parent`, dying `{"error":"invalid parent"}` — a message that
109
+ // never names the anchor. boardWorkspace was worse and silent: the whole comment
110
+ // became the workspace name, so rows would be minted somewhere that does not exist.
111
+ // A comment starts at `#` only when it is outside quotes and preceded by
112
+ // whitespace (or begins the value); inside quotes `#` is data.
63
113
  function unquote(raw) {
64
- if (raw.length > 1 && raw.startsWith('"') && raw.endsWith('"')) {
65
- return raw.slice(1, -1).replace(/\\(["\\])/g, '$1')
66
- }
67
- if (raw.length > 1 && raw.startsWith("'") && raw.endsWith("'")) {
68
- return raw.slice(1, -1).replace(/''/g, "'")
114
+ const q = raw[0]
115
+ if ((q === '"' || q === "'") && raw.length > 1) {
116
+ // walk to the matching close quote; everything after it is comment/whitespace
117
+ for (let i = 1; i < raw.length; i++) {
118
+ if (q === '"' && raw[i] === '\\') { i++; continue }
119
+ if (raw[i] !== q) continue
120
+ if (q === "'" && raw[i + 1] === "'") { i++; continue } // '' is an escaped quote
121
+ const body = raw.slice(1, i)
122
+ return q === '"' ? body.replace(/\\(["\\])/g, '$1') : body.replace(/''/g, "'")
123
+ }
124
+ return raw // unterminated quote — hand it back verbatim rather than guess
69
125
  }
70
- return raw
126
+ // plain scalar: a comment needs leading whitespace, so `a#b` stays `a#b`
127
+ const c = raw.search(/[ \t]#/)
128
+ return (c === -1 ? raw : raw.slice(0, c)).trim()
71
129
  }
72
130
  let outcome = ''
73
131
  const om = F.match(/^outcome:[ \t]*(?:\|[-+]?|>[-+]?)?[ \t]*(.*)$/m)
@@ -146,6 +204,42 @@ const sharedRecon = srm
146
204
  .filter((f) => f && fs.existsSync(path.join(repo, f)))
147
205
  : []
148
206
 
207
+ // ── the BOARD CONTRACT ──────────────────────────────────────────────────────
208
+ // A plan's cycles and the board's rows are the same work twice, and until this
209
+ // block existed nothing joined them: `batches:` held the ordering in markdown,
210
+ // D1 held it in `blocks` edges, and they drifted silently.
211
+ //
212
+ // It matters because `tasks:claim` is BLOCKER-GATED and reads the very `blocks`
213
+ // edge `tasks:depend` writes. A plan mirrored onto the board cannot be claimed
214
+ // out of order; an unmirrored one hands every agent every cycle at once.
215
+ //
216
+ // So emit both renderings and let the caller SEE the drift. `boardDrift` names
217
+ // every cycle the plan schedules that has no row — the ones invisible to every
218
+ // agent but the one running /do. It is reported, never fatal: a plan may be
219
+ // deliberately off-board, and a parser that refused would block that.
220
+ const anchorM = F.match(/^anchor:[ \t]*(.*)$/m)
221
+ const boardAnchor = anchorM ? unquote(anchorM[1].trim()) : ''
222
+ const bwM = F.match(/^board_workspace:[ \t]*(.*)$/m)
223
+ const boardWorkspace = bwM && bwM[1].trim() ? unquote(bwM[1].trim()) : 'one'
224
+
225
+ // cycle_rows: either inline flow (`cycle_rows: []`) or a block of
226
+ // ` - {cycle: C1, tid: "task:…"}` rows. Tolerant on purpose — this list is
227
+ // written back by a script and by hand, and a shape it cannot read must read as
228
+ // "no row" (surfaced in boardDrift) rather than crash a plan that is otherwise
229
+ // runnable.
230
+ const cycleRows = {}
231
+ const crStart = fLines.findIndex((l) => /^cycle_rows:[ \t]*$/.test(l))
232
+ if (crStart !== -1) {
233
+ for (let i = crStart + 1; i < fLines.length; i++) {
234
+ const l = fLines[i]
235
+ if (!/^[ \t]+-/.test(l)) { if (l.trim() && !/^[ \t]/.test(l)) break; else continue }
236
+ const c = l.match(/cycle:[ \t]*['"]?(C\d+)['"]?/)
237
+ const tid = l.match(/tid:[ \t]*['"]?(task:[0-9a-f]{24})['"]?/)
238
+ if (c && tid) cycleRows[c[1]] = tid[1]
239
+ }
240
+ }
241
+ const boardDrift = batches.flat().filter((c) => !cycleRows[c])
242
+
149
243
  process.stdout.write(JSON.stringify({
150
244
  slug,
151
245
  sharedRecon,
@@ -154,6 +248,10 @@ process.stdout.write(JSON.stringify({
154
248
  worktree: path.join(repo, '.do-worktrees', slug),
155
249
  outcome,
156
250
  batches,
251
+ boardAnchor,
252
+ boardWorkspace,
253
+ cycleRows,
254
+ boardDrift,
157
255
  openCycles,
158
256
  closedCycles,
159
257
  deps,
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # do-prove-selftest.sh — the red proof for do-prove.sh's argument contract.
3
+ # Invoked as `do-prove.sh --self-test`; $1 is the do-prove.sh under test.
4
+ #
5
+ # Asked for by .claude/improvements.queue.md:307 ("it has no --self-test, unlike
6
+ # its sibling gates ... or this regresses silently the way it arrived").
7
+ #
8
+ # Every GREEN check here is paired with a RED one that GUTS the guard it depends
9
+ # on and asserts the check then fails. A checker that stays green against gutted
10
+ # code proves nothing.
11
+ #
12
+ # Needs no live server: the parser half is pure argument handling, and the base
13
+ # half points at a closed port on purpose.
14
+ set -uo pipefail
15
+
16
+ TARGET="${1:-}"
17
+ [ -n "$TARGET" ] && [ -f "$TARGET" ] || { echo "selftest: need the path to do-prove.sh"; exit 2; }
18
+
19
+ pass=0; fail=0
20
+ ok() { echo " ok — $1"; pass=$((pass+1)); }
21
+ no() { echo " FAIL — $1"; fail=$((fail+1)); }
22
+
23
+ # A port nothing is listening on. Verified closed, not assumed.
24
+ DEAD=""
25
+ for p in 45871 45872 45873 45874; do
26
+ if ! curl -sf -o /dev/null --max-time 1 "http://127.0.0.1:$p/" 2>/dev/null; then DEAD="$p"; break; fi
27
+ done
28
+ [ -n "$DEAD" ] || { echo "selftest: could not find a closed port"; exit 2; }
29
+ DEADBASE="http://127.0.0.1:$DEAD"
30
+
31
+ # A gutted copy of do-prove.sh with one guard removed, laid out so its own
32
+ # ROOT-from-BASH_SOURCE derivation still works.
33
+ # A mutant must still PARSE. Deleting the body of an `if` leaves an orphaned
34
+ # `fi`, and bash exits 2 on a syntax error — which is indistinguishable from a
35
+ # usage error and would have let a broken mutant masquerade as a red proof.
36
+ # Measured while writing this file: both base-half mutants "failed" that way.
37
+ # So mutate by SUBSTITUTION (guard → `:`), and refuse a mutant that does not
38
+ # parse rather than scoring it.
39
+ gut() { # $1 = sed program → echoes the path of the mutant, or empty on failure
40
+ local d; d="$(mktemp -d)"
41
+ mkdir -p "$d/.claude/scripts"
42
+ sed "$1" "$TARGET" > "$d/.claude/scripts/do-prove.sh"
43
+ if ! cmp -s "$TARGET" "$d/.claude/scripts/do-prove.sh"; then :; else
44
+ echo "" ; return 0 # sed matched nothing — the guard moved
45
+ fi
46
+ bash -n "$d/.claude/scripts/do-prove.sh" 2>/dev/null || { echo ""; return 0; }
47
+ echo "$d/.claude/scripts/do-prove.sh"
48
+ }
49
+
50
+ echo "do-prove --self-test"
51
+ echo "[1/2] parser — an unrecognised FLAG is a hard error"
52
+
53
+ out="$(bash "$TARGET" --route / --bogus </dev/null 2>&1)"; rc=$?
54
+ [ "$rc" -eq 2 ] && ok "unknown flag --bogus exits 2 (got $rc)" \
55
+ || no "unknown flag --bogus should exit 2, got $rc"
56
+ case "$out" in *"unknown flag '--bogus'"*) ok "…and names the flag" ;;
57
+ *) no "output did not name the flag: $out" ;; esac
58
+
59
+ out="$(bash "$TARGET" --base </dev/null 2>&1)"; rc=$?
60
+ [ "$rc" -eq 2 ] && ok "--base with no value exits 2" || no "--base with no value: rc=$rc"
61
+ out="$(bash "$TARGET" --route </dev/null 2>&1)"; rc=$?
62
+ [ "$rc" -eq 2 ] && ok "--route with no value exits 2" || no "--route with no value: rc=$rc"
63
+
64
+ # The leniency that must SURVIVE: a bare token (cycle slug) is still ignored,
65
+ # not refused. do-w4-gates.sh:210 and do-smoke.sh:85-88 depend on this.
66
+ out="$(bash "$TARGET" some-cycle-slug --route / --base "$DEADBASE" </dev/null 2>&1)"; rc=$?
67
+ [ "$rc" -ne 2 ] && ok "a bare cycle slug is still tolerated (rc=$rc, not a usage error)" \
68
+ || no "a bare cycle slug was refused as a usage error — this breaks existing callers"
69
+ case "$out" in *"is not a path (cycle slug?)"*) ok "…and still says so" ;;
70
+ *) no "the lenient note is gone" ;; esac
71
+
72
+ # RED: remove the -*) branch and the flag guard must stop biting.
73
+ m="$(gut 's|^ exit 2 ;;$| shift ;;|')"
74
+ if [ -z "$m" ]; then no "RED PROOF FAILED: could not build the flag-guard mutant"; else
75
+ out="$(bash "$m" --route / --bogus --base "$DEADBASE" </dev/null 2>&1)"; rc=$?
76
+ [ "$rc" -ne 2 ] && ok "RED PROOF: gutted flag guard no longer exits 2 (got $rc) — check 1 is live" \
77
+ || no "RED PROOF FAILED: mutant still exits 2; check 1 proves nothing"
78
+ fi
79
+
80
+ echo "[2/2] base — --base pins the leg, and an unrun proof is not a pass"
81
+
82
+ out="$(bash "$TARGET" --route / --base "$DEADBASE" </dev/null 2>&1)"; rc=$?
83
+ [ "$rc" -ne 0 ] && ok "explicit --base that answers nothing exits non-zero (got $rc)" \
84
+ || no "explicit --base answered nothing and still exited 0 — the fail-open is back"
85
+ case "$out" in *"an unrun proof is not a pass"*) ok "…and says why" ;;
86
+ *) no "no unrun message: $out" ;; esac
87
+ case "$out" in *"https://one.ie"*) no "--base did not pin the fallback — it reached for https://one.ie" ;;
88
+ *) ok "--base pinned the fallback (production never named)" ;; esac
89
+
90
+ # RED: remove the fallback pin → the mutant must reach for https://one.ie.
91
+ m="$(gut 's|^ PROD_URL="\$BASE_URL"$| :|')"
92
+ if [ -z "$m" ]; then no "RED PROOF FAILED: could not build the pin mutant"; else
93
+ out="$(bash "$m" --route / --base "$DEADBASE" </dev/null 2>&1)"
94
+ case "$out" in *"https://one.ie"*) ok "RED PROOF: unpinned mutant reaches production — the pin is load-bearing" ;;
95
+ *) no "RED PROOF FAILED: mutant did not reach production; the pin check proves nothing" ;; esac
96
+ fi
97
+
98
+ # RED: remove the unrun-is-not-a-pass block → the mutant must go back to exit 0.
99
+ m="$(gut 's|^ exit 1$| :|')"
100
+ if [ -z "$m" ]; then no "RED PROOF FAILED: could not build the fail-open mutant"; else
101
+ out="$(bash "$m" --route / --base "$DEADBASE" </dev/null 2>&1)"; rc=$?
102
+ if [ "$rc" -eq 0 ]; then ok "RED PROOF: gutted mutant exits 0 on an unreachable base — the guard is live"
103
+ else no "RED PROOF FAILED: mutant still exits $rc; the fail-open check proves nothing"; fi
104
+ fi
105
+
106
+ echo
107
+ echo "do-prove --self-test: $pass passed, $fail failed"
108
+ [ "$fail" -eq 0 ] || exit 1
@@ -4,7 +4,8 @@
4
4
  # (chrome-headless-shell page check via chrome.mjs, curl fallback against prod).
5
5
  #
6
6
  # Usage:
7
- # do-prove.sh [--route <path>]... [<changed-path>|<cycle-slug>]...
7
+ # do-prove.sh [--route <path>]... [--base <url>] [<changed-path>|<cycle-slug>]...
8
+ # do-prove.sh --self-test
8
9
  # git diff --name-only | do-prove.sh
9
10
  #
10
11
  # Arg contract:
@@ -12,10 +13,36 @@
12
13
  # - Positional args that do NOT exist on disk (e.g. a cycle slug) are ignored
13
14
  # for surface detection; changed paths then come from `git diff --name-only HEAD`.
14
15
  # - --route <path> (repeatable) forces frontend surface + names the route(s) to prove.
16
+ # - --base <url> proves against <url> instead of the default localhost base.
15
17
  # - stdin (non-tty) supplies changed paths when no positional paths are given.
18
+ # - AN UNRECOGNISED FLAG IS A HARD ERROR (exit 2). It was not, until 2026-09-07,
19
+ # and that is how the launch promise came to prove a laptop. `text/bring-one-to-life.md`
20
+ # deliverable 6 read `--route /keys --base https://one.ie`; `--base` did not
21
+ # exist, so BOTH tokens fell through the lenient `*)` branch as "not a path
22
+ # (cycle slug?)" and the run silently used the localhost default. Measured that
23
+ # morning with a dev server up on :4321:
24
+ # PROVE: note — '--base' is not a path (cycle slug?); …
25
+ # PROVE: ok / — http=200, 0 console/js errors (dev http://localhost:4321)
26
+ # PROVE: pass (1 route(s) proven) ← exit 0, prod never contacted
27
+ # The leniency is deliberate for BARE tokens (a cycle slug is passed
28
+ # legitimately — do-w4-gates.sh:210, do-smoke.sh:85-88). It is never correct
29
+ # for a token starting with `-`: nobody passes a flag they do not mean.
30
+ #
31
+ # --base semantics (matches land.sh:344-345, which pins both bases by hand and
32
+ # says why): --base PINS THE FALLBACK TOO, unless PROVE_PROD_URL is explicitly
33
+ # exported. Setting only the dev base would rebuild the same defect one layer
34
+ # down — an unreachable `--base https://staging.x` would fall through to
35
+ # PROD_URL and prove PRODUCTION while reporting it as staging.
36
+ # And an explicit --base that ANSWERS NOTHING exits non-zero rather than
37
+ # `PROVE: skipped` (exit 0): do-promise-settle.sh runs a promise's proof: string
38
+ # verbatim and reads the exit code, so a skipped prod probe would settle the
39
+ # promise KEPT with the site down. An unrun gate is not a pass. Bare invocations
40
+ # keep the old skip-is-0 semantics — land.sh:346-350 reads the ROUTE COUNT and
41
+ # factory-walk.sh:294 matches `PROVE: skipped` → unrun, and both rely on it.
16
42
  #
17
43
  # Env:
18
- # PROVE_BASE_URL dev base for browser check (default http://localhost:4321)
44
+ # PROVE_BASE_URL dev base for browser check (default http://localhost:4321).
45
+ # `--base <url>` is the flag form of exactly this, and wins.
19
46
  # PROVE_PROD_URL prod fallback base (default https://one.ie)
20
47
  # PROVE_CHECK_CMD optional check command for non-frontend surfaces; if set and
21
48
  # it fails, do-prove.sh exits nonzero.
@@ -44,7 +71,10 @@
44
71
  #
45
72
  # Exit codes: 0 = pass or skipped (no reachable environment) · 1 = a reachable
46
73
  # page failed (HTTP >= 500 / 404 / status 0 / console+JS errors / landed on a
47
- # different path than requested) or declared check failed.
74
+ # different path than requested), a declared check failed, or an explicit --base
75
+ # answered nothing · 2 = usage error (unknown flag, or a flag missing its value).
76
+ #
77
+ # --self-test drives both halves red and green with no live server required.
48
78
  set -euo pipefail
49
79
 
50
80
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -54,11 +84,34 @@ BROWSER_CHECK="$ROOT/.claude/scripts/chrome.mjs"
54
84
 
55
85
  routes=()
56
86
  paths=()
87
+ base_explicit=0
88
+ self_test=0
89
+
90
+ prove_usage() {
91
+ echo "PROVE: usage: do-prove.sh [--route <path>]... [--base <url>] [--self-test] [<changed-path>|<cycle-slug>]..."
92
+ }
93
+
57
94
  while [ "$#" -gt 0 ]; do
58
95
  case "$1" in
59
96
  --route)
60
- [ "$#" -ge 2 ] || { echo "PROVE: --route requires a value"; exit 1; }
97
+ [ "$#" -ge 2 ] || { echo "PROVE: --route requires a value"; exit 2; }
61
98
  routes+=("$2"); shift 2 ;;
99
+ --base)
100
+ # The flag form of PROVE_BASE_URL. Trailing slash stripped so
101
+ # "$BASE_URL$route" never produces a double slash.
102
+ [ "$#" -ge 2 ] || { echo "PROVE: --base requires a value"; exit 2; }
103
+ BASE_URL="${2%/}"; base_explicit=1; shift 2 ;;
104
+ --self-test)
105
+ self_test=1; shift ;;
106
+ -h|--help)
107
+ prove_usage; exit 0 ;;
108
+ -*)
109
+ # THE ROOT CAUSE, closed. A token that looks like a flag and is not one is
110
+ # a caller asking for something this script cannot do — never a cycle slug.
111
+ # Silently continuing is how `--base https://one.ie` became a no-op.
112
+ echo "PROVE: unknown flag '$1'" >&2
113
+ prove_usage >&2
114
+ exit 2 ;;
62
115
  *)
63
116
  # A real path (exists on disk relative to root or cwd) counts as a changed
64
117
  # path; anything else (a cycle slug) is noted and ignored for detection.
@@ -71,6 +124,17 @@ while [ "$#" -gt 0 ]; do
71
124
  esac
72
125
  done
73
126
 
127
+ # An explicit --base means "prove HERE, nowhere else". Only an explicitly
128
+ # exported PROVE_PROD_URL may name a different fallback (land.sh does exactly
129
+ # that, pinning both to the dev URL).
130
+ if [ "$base_explicit" -eq 1 ] && [ -z "${PROVE_PROD_URL:-}" ]; then
131
+ PROD_URL="$BASE_URL"
132
+ fi
133
+
134
+ if [ "$self_test" -eq 1 ]; then
135
+ exec bash "$ROOT/.claude/scripts/do-prove-selftest.sh" "$ROOT/.claude/scripts/do-prove.sh"
136
+ fi
137
+
74
138
  # stdin (piped) paths
75
139
  if [ "${#paths[@]}" -eq 0 ] && [ ! -t 0 ]; then
76
140
  while IFS= read -r l; do [ -n "$l" ] && paths+=("$l"); done || true
@@ -265,6 +329,11 @@ report_redirect() { # $1 = route, $2 = landed path, $3 = leg label
265
329
  esac
266
330
  }
267
331
 
332
+ # The result line used to hardcode the word "dev" — so a production run
333
+ # printed "(dev https://one.ie)" on the very receipt someone reads to decide
334
+ # whether the site is live. Name the leg for what it is.
335
+ if _prove_local_base; then LEG="dev $BASE_URL"; else LEG="base $BASE_URL"; fi
336
+
268
337
  fail=0
269
338
  checked=0
270
339
  for route in "${routes[@]}"; do
@@ -341,14 +410,14 @@ for route in "${routes[@]}"; do
341
410
  # the browser leg never did, so a nonexistent route proved itself green
342
411
  # whenever the page also logged no errors. 401/403 stay ok — gated, not gone.
343
412
  if [ -n "$final" ] && [ "$landed" != "$(path_of "$route")" ]; then
344
- report_redirect "$route" "$landed" "dev $BASE_URL"
413
+ report_redirect "$route" "$landed" "$LEG"
345
414
  fail=1
346
415
  elif [ "$status" -ge 500 ] || [ "$status" -eq 404 ] || [ "$errs" -gt 0 ]; then
347
- echo "PROVE: FAIL $route — http=$status console/js errors=$errs (dev $BASE_URL)"
416
+ echo "PROVE: FAIL $route — http=$status console/js errors=$errs ($LEG)"
348
417
  printf '%s' "$out" | jq -r "$counted[]" 2>/dev/null | head -5 | sed 's/^/PROVE: error: /'
349
418
  fail=1
350
419
  else
351
- echo "PROVE: ok $route — http=$status, 0 console/js errors (dev $BASE_URL)"
420
+ echo "PROVE: ok $route — http=$status, 0 console/js errors ($LEG)"
352
421
  fi
353
422
  # Never silent: an ignored error is still an error someone should see.
354
423
  ign="$(printf '%s' "$out" | jq -r "$ignored" 2>/dev/null || echo 0)"
@@ -372,11 +441,11 @@ for route in "${routes[@]}"; do
372
441
  # running a tree without the page, and that line was the 17th leg of a
373
442
  # promise's proof. 401/403 stay ok: the route answered, it is merely gated.
374
443
  if [ -n "$effective" ] && [ "$landed" != "$(path_of "$route")" ]; then
375
- report_redirect "$route" "$landed" "dev $BASE_URL, curl"; fail=1
444
+ report_redirect "$route" "$landed" "$LEG, curl"; fail=1
376
445
  elif [ "$code" -ge 500 ] || [ "$code" -eq 404 ]; then
377
- echo "PROVE: FAIL $route — http=$code (dev $BASE_URL, curl)"; fail=1
446
+ echo "PROVE: FAIL $route — http=$code ($LEG, curl)"; fail=1
378
447
  else
379
- echo "PROVE: ok $route — http=$code (dev $BASE_URL, curl)"
448
+ echo "PROVE: ok $route — http=$code ($LEG, curl)"
380
449
  fi
381
450
  fi
382
451
  fi
@@ -405,6 +474,13 @@ if [ "$fail" -eq 1 ]; then
405
474
  echo "PROVE: fail"; exit 1
406
475
  fi
407
476
  if [ "$checked" -eq 0 ]; then
477
+ # An explicitly named base that answered nothing is an UNRUN PROOF, and
478
+ # do-promise-settle.sh reads this exit code verbatim. Exit 0 here would settle
479
+ # a promise KEPT because the site was unreachable.
480
+ if [ "$base_explicit" -eq 1 ]; then
481
+ echo "PROVE: FAIL — --base $BASE_URL answered nothing; an unrun proof is not a pass"
482
+ exit 1
483
+ fi
408
484
  echo "PROVE: skipped (no reachable environment)"; exit 0
409
485
  fi
410
486
  echo "PROVE: pass ($checked route(s) proven)"; exit 0