@oneie/claude 0.6.0 → 0.7.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 (111) hide show
  1. package/agents/w1-recon.md +9 -4
  2. package/agents/w2-decide.md +51 -28
  3. package/agents/w3-edit.md +70 -78
  4. package/agents/w4-verify.md +164 -69
  5. package/commands/browser.md +98 -35
  6. package/commands/cc-connect.md +6 -0
  7. package/commands/chat.md +13 -1
  8. package/commands/close.md +45 -19
  9. package/commands/create.md +20 -10
  10. package/commands/db-sync.md +5 -1
  11. package/commands/deploy.md +174 -87
  12. package/commands/do-autonomous.md +31 -20
  13. package/commands/do-improve.md +4 -4
  14. package/commands/do-show.md +4 -3
  15. package/commands/do.md +53 -13
  16. package/commands/go.md +75 -0
  17. package/commands/improve.md +4 -2
  18. package/commands/kill.md +6 -8
  19. package/commands/notify.md +15 -2
  20. package/commands/oo-push.md +24 -9
  21. package/commands/rag.md +6 -0
  22. package/commands/release.md +46 -25
  23. package/commands/restart.md +5 -4
  24. package/commands/see.md +45 -35
  25. package/commands/skill-create.md +38 -5
  26. package/commands/sync.md +21 -11
  27. package/hooks/scripts/auto-continue.sh +70 -0
  28. package/hooks/scripts/branch-pin.sh +85 -0
  29. package/hooks/scripts/git-add-guard.sh +16 -5
  30. package/hooks/scripts/post-edit-check.sh +16 -0
  31. package/hooks/scripts/session-start.sh +13 -8
  32. package/hooks/scripts/sync-priority-todo.sh +5 -0
  33. package/package.json +1 -1
  34. package/rules/api.md +9 -0
  35. package/rules/design.md +13 -3
  36. package/rules/documentation.md +30 -71
  37. package/rules/engine.md +8 -1
  38. package/rules/ui.md +5 -1
  39. package/scripts/__pycache__/do-rank.cpython-313.pyc +0 -0
  40. package/scripts/browser-check.mjs +44 -103
  41. package/scripts/cc-events-proof.sh +13 -12
  42. package/scripts/chrome.mjs +381 -0
  43. package/scripts/do-analyze.sh +109 -4
  44. package/scripts/do-auto.sh +350 -43
  45. package/scripts/do-consumer-sweep.sh +72 -0
  46. package/scripts/do-derives-check.sh +88 -0
  47. package/scripts/do-fleet.sh +50 -1
  48. package/scripts/do-killswitch-audit.py +33 -14
  49. package/scripts/do-promise-lint.sh +150 -4
  50. package/scripts/do-promise-settle.sh +131 -9
  51. package/scripts/do-prove.sh +197 -17
  52. package/scripts/do-rank.py +606 -20
  53. package/scripts/do-rubric.py +21 -2
  54. package/scripts/do-signal.sh +157 -17
  55. package/scripts/do-smoke.sh +9 -0
  56. package/scripts/do-substrate-check.sh +49 -0
  57. package/scripts/do-tasks-wire-check.sh +81 -0
  58. package/scripts/do-tier.sh +90 -5
  59. package/scripts/do-ui-gate.sh +332 -0
  60. package/scripts/do-untracked-gate.sh +231 -0
  61. package/scripts/do-walk.sh +344 -0
  62. package/scripts/factory-check.sh +747 -0
  63. package/scripts/factory-repo.sh +1361 -0
  64. package/scripts/factory-tasks-check.sh +85 -0
  65. package/scripts/outcome-pull.ts +279 -0
  66. package/scripts/promise-manifest.mjs +167 -0
  67. package/scripts/reconcile-allow.txt +5 -0
  68. package/scripts/rubric-weights.json +64 -0
  69. package/scripts/spine-canary.sh +20 -0
  70. package/scripts/typedb-cluster-status.sh +128 -0
  71. package/scripts/typedb-env.sh +182 -0
  72. package/scripts/typedb-flap-recorder.sh +85 -0
  73. package/scripts/typedb-probes/containment-probe.py +88 -0
  74. package/scripts/typedb-probes/panic-probe-version.py +92 -0
  75. package/scripts/typedb-probes/panic-probe.py +77 -0
  76. package/scripts/typedb-scratch.sh +73 -0
  77. package/scripts/urls-lint.sh +58 -0
  78. package/scripts/w4-rubric.ts +96 -12
  79. package/scripts/wf-check.mjs +75 -0
  80. package/skills/ai-ui/SKILL.md +1 -1
  81. package/skills/astro/SKILL.md +177 -132
  82. package/skills/astro/add-content-collection.md +29 -13
  83. package/skills/astro/check-build.md +19 -13
  84. package/skills/astro/create-component.md +36 -9
  85. package/skills/astro/create-page.md +39 -10
  86. package/skills/astro/optimize-performance.md +36 -8
  87. package/skills/cli/SKILL.md +254 -220
  88. package/skills/dev/SKILL.md +1 -1
  89. package/skills/directory-autofill/SKILL.md +68 -0
  90. package/skills/docs/SKILL.md +4 -4
  91. package/skills/mcp/SKILL.md +186 -156
  92. package/skills/promise-make/SKILL.md +2 -2
  93. package/skills/puck/SKILL.md +465 -250
  94. package/skills/react19/SKILL.md +196 -296
  95. package/skills/reactflow/SKILL.md +387 -412
  96. package/skills/sdk/SKILL.md +167 -106
  97. package/skills/shadcn/SKILL.md +245 -201
  98. package/skills/signal/SKILL.md +5 -0
  99. package/skills/sui/SKILL.md +6 -1
  100. package/skills/tutorial/SKILL.md +15 -0
  101. package/skills/typedb/SKILL.md +427 -217
  102. package/skills/typedb/examples/python-patterns.py +11 -4
  103. package/skills/typedb/examples/query-patterns.tql +8 -3
  104. package/skills/typedb/examples/schema-patterns.tql +4 -2
  105. package/skills/typedb/reference/migration-2x-3x.md +19 -1
  106. package/skills/typedb/reference/python-driver.md +17 -1
  107. package/skills/voice/SKILL.md +117 -0
  108. package/skills/writer/SKILL.md +29 -12
  109. package/templates/template-feature.md +26 -1
  110. package/templates/template-tests.md +4 -0
  111. package/templates/template-todo.md +65 -10
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * chrome.mjs — headless-shell browser driver. Replaces the `claude-in-chrome`
4
+ * MCP tools for every non-interactive job: PROVE gates, page diagnostics,
5
+ * scraping, form drills, screenshots.
6
+ *
7
+ * Engine: chrome-headless-shell, launched through Playwright's
8
+ * `chromium-headless-shell` channel. No extension, no Chrome profile, no
9
+ * running desktop browser, no human in the loop. One process per invocation.
10
+ *
11
+ * Usage:
12
+ * node .claude/scripts/chrome.mjs <url> [flags]
13
+ *
14
+ * Flags:
15
+ * --text include page innerText (truncated, see --max)
16
+ * --html include page HTML (truncated, see --max)
17
+ * --sel <css> scope --text/--html to a selector
18
+ * --screenshot [path] PNG (default /tmp/chrome-shot.png); --full-page for the whole document
19
+ * --console include ALL console lines (errors are always included)
20
+ * --network include every request/response (default: /api/ only, on --network-all)
21
+ * --network-all widen --network past /api/
22
+ * --eval <js> evaluate an expression in the page, return its value
23
+ * --do <json> run a step list — see STEPS below
24
+ * --send <msg> chat drill: fill the first textarea, Enter, capture the SSE stream
25
+ * --wait <ms> settle time after load (default 2500)
26
+ * --timeout <ms> navigation timeout (default 20000)
27
+ * --state <path> persist cookies + localStorage to <path> and reuse them next
28
+ * run — the one thing a fresh process cannot have on its own
29
+ * --cookie <n=v> add a cookie for the target host (repeatable)
30
+ * --header <k: v> extra HTTP header (repeatable)
31
+ * --ua <string> user agent
32
+ * --viewport <WxH> viewport (default 1280x800)
33
+ * --headed use full Chromium with a visible window instead of the shell
34
+ * --max <n> truncation ceiling for text/html/eval (default 4000)
35
+ *
36
+ * STEPS (--do) — a JSON array, executed in order, each a single-key object:
37
+ * {"goto":"https://…"} navigate
38
+ * {"click":"<css|text=…>"} click
39
+ * {"fill":"<css>","value":"…"} fill an input
40
+ * {"press":"Enter","sel":"<css>"} key press (sel optional → page-level)
41
+ * {"select":"<css>","value":"…"} <select> option
42
+ * {"dblclick":"<css>"} double click
43
+ * {"hover":"<css>"} hover
44
+ * {"check":"<css>","value":false} check / uncheck a box (value defaults true)
45
+ * {"upload":"<css>","files":[…]} set file inputs
46
+ * {"scroll":600} | {"scroll":"<css>"} wheel by px, or scroll a node into view
47
+ * {"wait":1500} sleep ms
48
+ * {"waitFor":"<css>"} wait for a selector
49
+ * {"eval":"<js>"} evaluate, appended to steps[].result
50
+ * {"screenshot":"/path.png"} capture mid-run
51
+ * Every step reports {step, ok, error?, result?} — a failed step does not abort the run.
52
+ *
53
+ * Env:
54
+ * PROVE_SESSION_COOKIE "name=value" — same contract as browser-check.mjs
55
+ * ONE_PLAYWRIGHT_DIR explicit path to a node_modules dir holding playwright
56
+ *
57
+ * Output: one JSON report on stdout. Exit 0 = the run completed (read httpStatus
58
+ * and jsErrors for the verdict); exit 1 = the browser could not run at all.
59
+ */
60
+
61
+ import { createRequire } from 'node:module'
62
+ import { existsSync, readdirSync } from 'node:fs'
63
+ import { dirname, join, resolve } from 'node:path'
64
+ import { fileURLToPath } from 'node:url'
65
+ import { execFileSync } from 'node:child_process'
66
+
67
+ const HERE = dirname(fileURLToPath(import.meta.url))
68
+ const REPO = resolve(HERE, '../..')
69
+
70
+ // ---------------------------------------------------------------- playwright
71
+ // No package name is hardcoded — this must work in any tree that has Playwright
72
+ // somewhere, at the root or in a workspace member. A linked worktree has no
73
+ // node_modules of its own, so the main worktree is probed too, via git's common
74
+ // dir. Never hardcode an absolute user path.
75
+ const SKIP_DIR = /^(\.|node_modules$|dist$|build$|target$|\.git$)/
76
+
77
+ function subdirs(dir) {
78
+ try {
79
+ return readdirSync(dir, { withFileTypes: true })
80
+ .filter((d) => d.isDirectory() && !SKIP_DIR.test(d.name))
81
+ .map((d) => join(dir, d.name))
82
+ } catch { return [] }
83
+ }
84
+
85
+ function playwrightRoots() {
86
+ const roots = []
87
+ if (process.env.ONE_PLAYWRIGHT_DIR) roots.push(process.env.ONE_PLAYWRIGHT_DIR)
88
+
89
+ const trees = [REPO]
90
+ try {
91
+ const common = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
92
+ cwd: REPO, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
93
+ }).trim()
94
+ if (common) trees.push(dirname(common))
95
+ } catch { /* not a git tree — fine */ }
96
+
97
+ // Root first, then workspace members up to two levels down. Bounded on
98
+ // purpose: deep enough for `<app>/<surface>/node_modules`, shallow enough
99
+ // never to walk a whole tree.
100
+ for (const t of trees) {
101
+ roots.push(join(t, 'node_modules'))
102
+ for (const d1 of subdirs(t)) {
103
+ roots.push(join(d1, 'node_modules'))
104
+ for (const d2 of subdirs(d1)) roots.push(join(d2, 'node_modules'))
105
+ }
106
+ }
107
+ roots.push('/tmp/node_modules') // legacy browser-check location
108
+ return [...new Set(roots)]
109
+ }
110
+
111
+ function loadPlaywright() {
112
+ for (const root of playwrightRoots()) {
113
+ const entry = join(root, 'playwright', 'index.js')
114
+ if (!existsSync(entry)) continue
115
+ try {
116
+ return { mod: createRequire(join(root, 'noop.js'))('playwright'), from: root }
117
+ } catch { /* try the next root */ }
118
+ }
119
+ return null
120
+ }
121
+
122
+ const pw = loadPlaywright()
123
+ if (!pw) {
124
+ console.error(JSON.stringify({
125
+ error: 'playwright_not_found',
126
+ searched: playwrightRoots(),
127
+ fix: 'install playwright in this tree, then: npx playwright install chromium-headless-shell',
128
+ }, null, 2))
129
+ process.exit(1)
130
+ }
131
+ const { chromium } = pw.mod
132
+
133
+ // ---------------------------------------------------------------------- args
134
+ const argv = process.argv.slice(2)
135
+ const flag = (name) => argv.includes(`--${name}`)
136
+ const val = (name, fallback = null) => {
137
+ const i = argv.indexOf(`--${name}`)
138
+ if (i === -1) return fallback
139
+ const v = argv[i + 1]
140
+ return v === undefined || v.startsWith('--') ? true : v
141
+ }
142
+ const all = (name) => argv.flatMap((a, i) => (a === `--${name}` && argv[i + 1] && !argv[i + 1].startsWith('--') ? [argv[i + 1]] : []))
143
+
144
+ const url = argv.find((a) => /^(https?|file):\/\//.test(a)) ?? 'http://localhost:4321'
145
+ const MAX = Number(val('max', 4000)) || 4000
146
+ const waitMs = Number(val('wait', 2500)) || 0
147
+ const timeout = Number(val('timeout', 20000)) || 20000
148
+ const cut = (s) => (typeof s === 'string' && s.length > MAX ? `${s.slice(0, MAX)}…[+${s.length - MAX}]` : s)
149
+
150
+ let steps = []
151
+ const doArg = val('do')
152
+ if (typeof doArg === 'string') {
153
+ try { steps = JSON.parse(doArg) } catch (e) {
154
+ console.error(JSON.stringify({ error: 'bad_steps_json', detail: e.message }, null, 2))
155
+ process.exit(1)
156
+ }
157
+ if (!Array.isArray(steps)) { console.error(JSON.stringify({ error: 'steps_must_be_array' })); process.exit(1) }
158
+ }
159
+
160
+ // ------------------------------------------------------------------- browser
161
+ const headed = flag('headed')
162
+ let browser
163
+ try {
164
+ browser = headed
165
+ ? await chromium.launch({ headless: false })
166
+ : await chromium.launch({ channel: 'chromium-headless-shell' })
167
+ } catch (e) {
168
+ // Channel missing (shell not installed) → plain headless chromium still works.
169
+ try {
170
+ browser = await chromium.launch({ headless: !headed })
171
+ } catch (e2) {
172
+ console.error(JSON.stringify({
173
+ error: 'browser_launch_failed', shell: e.message, fallback: e2.message,
174
+ fix: 'npx playwright install chromium-headless-shell',
175
+ }, null, 2))
176
+ process.exit(1)
177
+ }
178
+ }
179
+
180
+ // --state gives this tool the one thing a fresh process cannot have: a session
181
+ // that survives the run. Sign in once with --state, and every later invocation
182
+ // reuses the cookies and localStorage instead of re-authenticating.
183
+ const statePath = typeof val('state') === 'string' ? val('state') : null
184
+ const stateLoaded = Boolean(statePath && existsSync(statePath))
185
+
186
+ const [vw, vh] = String(val('viewport', '1280x800')).split('x').map(Number)
187
+ const context = await browser.newContext({
188
+ viewport: { width: vw || 1280, height: vh || 800 },
189
+ ...(stateLoaded ? { storageState: statePath } : {}),
190
+ ...(typeof val('ua') === 'string' ? { userAgent: val('ua') } : {}),
191
+ ...(all('header').length
192
+ ? { extraHTTPHeaders: Object.fromEntries(all('header').map((h) => {
193
+ const i = h.indexOf(':')
194
+ return [h.slice(0, i).trim(), h.slice(i + 1).trim()]
195
+ })) }
196
+ : {}),
197
+ })
198
+
199
+ const cookieSpecs = [...all('cookie'), ...(process.env.PROVE_SESSION_COOKIE ? [process.env.PROVE_SESSION_COOKIE] : [])]
200
+ if (cookieSpecs.length && /^https?:/.test(url)) { // a file:// url has no host to scope a cookie to
201
+ const { hostname } = new URL(url)
202
+ await context.addCookies(cookieSpecs.flatMap((spec) => {
203
+ const [name, ...rest] = spec.split('=')
204
+ const value = rest.join('=')
205
+ return name && value ? [{ name, value, domain: hostname, path: '/' }] : []
206
+ }))
207
+ }
208
+
209
+ const page = await context.newPage()
210
+
211
+ // ------------------------------------------------------------------ recorders
212
+ const consoleLogs = []
213
+ const jsErrors = []
214
+ const requests = []
215
+ page.on('console', (m) => consoleLogs.push({ type: m.type(), text: m.text().slice(0, 500) }))
216
+ page.on('pageerror', (e) => jsErrors.push(String(e).slice(0, 500)))
217
+
218
+ const wantNetwork = flag('network') || flag('network-all')
219
+ const netAll = flag('network-all')
220
+ if (wantNetwork) {
221
+ page.on('response', async (r) => {
222
+ const u = r.url()
223
+ if (!netAll && !u.includes('/api/')) return
224
+ requests.push({ method: r.request().method(), status: r.status(), url: u.split('?')[0] })
225
+ })
226
+ }
227
+
228
+ // SSE/chat capture — kept byte-for-byte compatible with browser-check.mjs so
229
+ // `--send` reports the same chatCapture shape /do PROVE steps already read.
230
+ const sendMsg = typeof val('send') === 'string' ? val('send') : null
231
+ if (sendMsg) {
232
+ await page.addInitScript(() => {
233
+ const orig = window.fetch
234
+ window.__chatCapture = []
235
+ window.fetch = async (...args) => {
236
+ const u = args[0]?.toString() || ''
237
+ if (u.includes('/api/chat') && !u.includes('warmup') && !u.includes('tts')) {
238
+ const method = args[1]?.method || 'GET'
239
+ const body = typeof args[1]?.body === 'string' ? args[1].body : ''
240
+ if (method === 'POST') {
241
+ window.__chatCapture.push({ type: 'request', body: body.substring(0, 200) })
242
+ const resp = await orig(...args)
243
+ const reader = resp.clone().body?.getReader()
244
+ if (reader) {
245
+ let chunks = ''
246
+ for (;;) {
247
+ const { done, value } = await reader.read()
248
+ if (done) break
249
+ chunks += new TextDecoder().decode(value)
250
+ }
251
+ window.__chatCapture.push({ type: 'stream', status: resp.status, body: chunks.substring(0, 800) })
252
+ }
253
+ return resp
254
+ }
255
+ }
256
+ return orig(...args)
257
+ }
258
+ })
259
+ }
260
+
261
+ // ---------------------------------------------------------------------- load
262
+ let httpStatus = 0
263
+ let navError = null
264
+ try {
265
+ const res = await page.goto(url, { waitUntil: 'load', timeout })
266
+ httpStatus = res ? res.status() : 0
267
+ } catch (e) {
268
+ navError = e.message
269
+ }
270
+ if (waitMs) await page.waitForTimeout(waitMs)
271
+
272
+ const title = await page.title().catch(() => null)
273
+ const railBefore = sendMsg
274
+ ? await page.evaluate(() => document.querySelector('.chat-rail')?.innerText?.substring(0, 200) || null).catch(() => null)
275
+ : undefined
276
+
277
+ // --------------------------------------------------------------------- steps
278
+ const stepResults = []
279
+ for (const [i, step] of steps.entries()) {
280
+ const kind = Object.keys(step)[0]
281
+ const rec = { step: i, kind, ok: true }
282
+ try {
283
+ if (step.goto) { const r = await page.goto(step.goto, { waitUntil: 'load', timeout }); rec.result = r?.status() ?? 0 }
284
+ else if (step.click) await page.click(step.click, { timeout: 8000 })
285
+ else if (step.fill !== undefined) await page.fill(step.fill, String(step.value ?? ''), { timeout: 8000 })
286
+ else if (step.press) { step.sel ? await page.press(step.sel, step.press) : await page.keyboard.press(step.press) }
287
+ else if (step.select) await page.selectOption(step.select, String(step.value ?? ''))
288
+ else if (step.dblclick) await page.dblclick(step.dblclick, { timeout: 8000 })
289
+ else if (step.hover) await page.hover(step.hover, { timeout: 8000 })
290
+ else if (step.check) await page.setChecked(step.check, step.value !== false, { timeout: 8000 })
291
+ else if (step.upload) await page.setInputFiles(step.upload, [].concat(step.files ?? step.value ?? []), { timeout: 8000 })
292
+ else if (step.scroll !== undefined) {
293
+ // window.scrollBy, not mouse.wheel — the wheel needs a pointer parked over
294
+ // the document and silently scrolls nothing in the headless shell when it
295
+ // isn't (measured: {"scroll":1200} left scrollY at 0 on a 4000px page).
296
+ typeof step.scroll === 'string'
297
+ ? await page.evaluate((s) => document.querySelector(s)?.scrollIntoView({ block: 'center' }), step.scroll)
298
+ : await page.evaluate((px) => window.scrollBy(0, px), Number(step.scroll))
299
+ }
300
+ else if (step.wait) await page.waitForTimeout(Number(step.wait))
301
+ else if (step.waitFor) await page.waitForSelector(step.waitFor, { timeout: 10000 })
302
+ else if (step.eval) rec.result = cut(JSON.stringify(await page.evaluate(step.eval)))
303
+ else if (step.screenshot) { await page.screenshot({ path: step.screenshot, fullPage: flag('full-page') }); rec.result = step.screenshot }
304
+ else { rec.ok = false; rec.error = `unknown step kind: ${kind}` }
305
+ } catch (e) {
306
+ rec.ok = false
307
+ rec.error = e.message.split('\n')[0].slice(0, 300)
308
+ }
309
+ stepResults.push(rec)
310
+ }
311
+
312
+ // ---------------------------------------------------------------- chat drill
313
+ let chatCapture
314
+ let railAfter
315
+ if (sendMsg) {
316
+ const ta = await page.$('textarea')
317
+ if (ta) {
318
+ await ta.fill(sendMsg)
319
+ await ta.press('Enter')
320
+ await page.waitForTimeout(Number(val('send-wait', 12000)) || 12000)
321
+ railAfter = await page.evaluate(() => document.querySelector('.chat-rail')?.innerText?.substring(0, 400) || null)
322
+ }
323
+ chatCapture = await page.evaluate(() => window.__chatCapture || [])
324
+ }
325
+
326
+ // ------------------------------------------------------------------- extract
327
+ const sel = typeof val('sel') === 'string' ? val('sel') : null
328
+ let text
329
+ let html
330
+ if (flag('text')) {
331
+ text = cut(await page.evaluate((s) => (s ? document.querySelector(s)?.innerText : document.body?.innerText) || '', sel))
332
+ }
333
+ if (flag('html')) {
334
+ html = cut(await page.evaluate((s) => (s ? document.querySelector(s)?.outerHTML : document.documentElement?.outerHTML) || '', sel))
335
+ }
336
+
337
+ let evalResult
338
+ const evalJs = typeof val('eval') === 'string' ? val('eval') : null
339
+ if (evalJs) {
340
+ try { evalResult = cut(JSON.stringify(await page.evaluate(evalJs))) }
341
+ catch (e) { evalResult = `ERROR: ${e.message.split('\n')[0]}` }
342
+ }
343
+
344
+ let screenshot
345
+ if (flag('screenshot')) {
346
+ screenshot = typeof val('screenshot') === 'string' ? val('screenshot') : '/tmp/chrome-shot.png'
347
+ await page.screenshot({ path: screenshot, fullPage: flag('full-page') }).catch((e) => { screenshot = `ERROR: ${e.message}` })
348
+ }
349
+
350
+ let stateSaved
351
+ if (statePath) {
352
+ try { await context.storageState({ path: statePath }); stateSaved = statePath }
353
+ catch (e) { stateSaved = `ERROR: ${e.message}` }
354
+ }
355
+
356
+ await browser.close()
357
+
358
+ // -------------------------------------------------------------------- report
359
+ console.log(JSON.stringify({
360
+ url,
361
+ finalUrl: page.url(),
362
+ engine: headed ? 'chromium (headed)' : 'chrome-headless-shell',
363
+ playwrightFrom: pw.from,
364
+ stateLoaded: statePath ? stateLoaded : undefined,
365
+ stateSaved,
366
+ httpStatus,
367
+ navError,
368
+ title,
369
+ jsErrors,
370
+ consoleErrors: consoleLogs.filter((l) => l.type === 'error').map((l) => l.text),
371
+ console: flag('console') ? consoleLogs : undefined,
372
+ network: wantNetwork ? requests : undefined,
373
+ steps: steps.length ? stepResults : undefined,
374
+ chatCapture,
375
+ chatRailBefore: railBefore,
376
+ chatRailAfter: sendMsg ? railAfter : undefined,
377
+ text,
378
+ html,
379
+ eval: evalResult,
380
+ screenshot,
381
+ }, null, 2))
@@ -7,12 +7,29 @@
7
7
  # CRITICAL (exit 1, blocks build):
8
8
  # - deliverables: frontmatter is empty → plan ships nothing
9
9
  # - a ## C# cycle has no **Deliverable:** line → cycle ships nothing (coverage hole)
10
+ # - a ## C# cycle has no `- [ ] W` wave checkbox → do-auto/do-engine read the cycle as
11
+ # already-done and FALSE-COMPLETE the plan
12
+ # (built nothing; verified done-ui 2026-07-23)
10
13
  # HIGH (warn, does not block):
11
14
  # - a cycle has no demo:/Cycle outcome line → AC→test gap (no planned verification)
15
+ # - a promise deliverable's accept: appears in NO cycle → the promise ships broken even
16
+ # though every cycle closes green
17
+ # (verified factory 2026-07-28: stream,
18
+ # walk-speed, tracer had no cycle at all —
19
+ # they existed only in the todo's outcome:)
20
+ # `--strict-promise` promotes this to CRITICAL (exit 1).
12
21
  # Never edits.
13
- # Usage: do-analyze.sh <todo.md>
22
+ # Usage: do-analyze.sh <todo.md> [--strict-promise]
14
23
  set -euo pipefail
15
- todo="${1:?usage: do-analyze.sh <todo.md>}"
24
+ todo=""; strict=0
25
+ for a in "$@"; do
26
+ case "$a" in
27
+ --strict-promise) strict=1 ;;
28
+ -*) echo "CRITICAL: unknown flag: $a"; exit 1 ;;
29
+ *) [ -n "$todo" ] || todo="$a" ;;
30
+ esac
31
+ done
32
+ [ -n "$todo" ] || { echo "usage: do-analyze.sh <todo.md> [--strict-promise]"; exit 1; }
16
33
  [ -f "$todo" ] || { echo "CRITICAL: todo not found: $todo"; exit 1; }
17
34
 
18
35
  crit=0; high=0
@@ -30,7 +47,7 @@ fi
30
47
 
31
48
  # 2. every cycle ships a deliverable AND has a planned test. Walk each ## C# section
32
49
  # (header to the next ## header — ### W-waves and **bold** lines stay inside).
33
- ncyc=0; nodeliv=0; notest=0
50
+ ncyc=0; nodeliv=0; notest=0; nowave=0
34
51
  for c in $(grep -oE '^## C[0-9]+' "$todo" | grep -oE 'C[0-9]+'); do
35
52
  ncyc=$((ncyc+1))
36
53
  block=$(awk -v c="$c" '
@@ -41,12 +58,100 @@ for c in $(grep -oE '^## C[0-9]+' "$todo" | grep -oE 'C[0-9]+'); do
41
58
  printf '%s' "$block" | grep -qE '^\*\*Deliverable' || {
42
59
  echo "CRITICAL: $c has no **Deliverable:** line (cycle ships nothing — coverage hole)"; crit=1; nodeliv=$((nodeliv+1))
43
60
  }
61
+ # Wave-checkbox presence: do-auto.sh _sync_status (IC3) and do-engine.js openCyclesFromTodo
62
+ # decide a cycle is done when its ## C# SECTION has zero `- [ ] W` lines. A cycle with NO
63
+ # `- [ ] W`/`- [x] W`/`- [~] W` line at all is read as already-complete → the loop builds
64
+ # nothing and reports "plan complete". Refuse it before build.
65
+ printf '%s' "$block" | grep -qE '^[[:space:]]*- \[[ x~]\] W' || {
66
+ echo "CRITICAL: $c has no '- [ ] W' wave checkbox in its section — do-auto/do-engine will read it as already-done and false-complete the plan (add a '### Definition of done' with '- [ ] W1..W4')"; crit=1; nowave=$((nowave+1))
67
+ }
44
68
  printf '%s' "$block" | grep -qiE 'demo:|^\*\*Cycle outcome' || {
45
69
  echo "HIGH: $c has no planned test (demo: or **Cycle outcome:** line)"; high=$((high+1)); notest=$((notest+1))
46
70
  }
47
71
  done
48
72
 
73
+ # 3. PROMISE coverage: every deliverable in text/<slug>.md is served by some cycle.
74
+ # Checks 1-2 only ever compare the todo against ITSELF, so a plan can be internally
75
+ # consistent, pass ANALYZE, close every cycle green — and still leave the promise
76
+ # broken, discovered first at settle. This reads the promise.
77
+ # Matching rule: the deliverable's whole `accept:` command, whitespace-normalised, must
78
+ # appear inside a `## C<n>` SECTION (its demo.command or body). Frontmatter (where
79
+ # `outcome:` restates every accept: verbatim) and `Correct-course` blocks are NOT corpus —
80
+ # that is exactly where the three missing factory deliverables did appear, which is why
81
+ # the gap was invisible. Trailing boundary stops `... view` matching `... view-gaps`.
82
+ promise="$(dirname "$todo")/$(basename "$todo" -todo.md).md"
83
+ pndel=0; pcov=0; puncov=0
84
+ if [ -f "$promise" ]; then
85
+ accepts=$(awk '
86
+ /^---[[:space:]]*$/ { d++; if (d>=2) exit; next }
87
+ d!=1 { next }
88
+ /^deliverables:[[:space:]]*$/ { f=1; next }
89
+ f && /^[A-Za-z_][A-Za-z0-9_-]*:/ { f=0 }
90
+ !f { next }
91
+ /^[[:space:]]*-?[[:space:]]*item:[[:space:]]*/ {
92
+ it=$0; sub(/^[[:space:]]*-?[[:space:]]*item:[[:space:]]*/,"",it); item=unq(it); next
93
+ }
94
+ /^[[:space:]]*accept:[[:space:]]*/ {
95
+ ac=$0; sub(/^[[:space:]]*accept:[[:space:]]*/,"",ac); ac=unq(ac)
96
+ gsub(/[[:space:]]+/," ",ac); sub(/^ /,"",ac); sub(/ $/,"",ac)
97
+ if (ac != "") printf "%s\t%s\n", ac, item
98
+ }
99
+ # strip the YAML quote wrapper, then unescape \" so a single-quoted promise and a
100
+ # double-quoted todo demo of the SAME command still compare equal
101
+ function unq(s) {
102
+ sub(/[[:space:]]+$/,"",s)
103
+ if (s ~ /^".*"$/ || s ~ /^'"'"'.*'"'"'$/) s=substr(s,2,length(s)-2)
104
+ gsub(/\\"/,"\"",s)
105
+ return s
106
+ }
107
+ ' "$promise")
108
+ # cycle corpus: ## C<n> sections only, whitespace-normalised, one line per source line
109
+ corpus=$(awk '
110
+ /^---[[:space:]]*$/ { d++; next }
111
+ d<2 { next }
112
+ # depth-aware: a cycle section runs until a heading at the SAME-OR-SHALLOWER depth.
113
+ # `#### W3 edit` inside `### C1` stays in; `## Cycles`/`### C2` ends it. Correct-course
114
+ # is excluded at ANY depth (a `#### Correct-course` nested inside a cycle would
115
+ # otherwise re-open the exact blind spot this check exists to close).
116
+ /^#+[[:space:]]*C[0-9]+([[:space:]]|$)/ { match($0,/^#+/); opd=RLENGTH; inc=1; next }
117
+ tolower($0) ~ /^#+[[:space:]]*.*correct-course/ { inc=0; next }
118
+ /^#+[[:space:]]/ { match($0,/^#+/); if (RLENGTH<=opd) { inc=0; next } }
119
+ inc { gsub(/\\"/,"\""); gsub(/[[:space:]]+/," "); sub(/^ /,""); sub(/ $/,""); if ($0 != "") print }
120
+ ' "$todo")
121
+ pndel=$(printf '%s' "$accepts" | grep -c . || true)
122
+ # cycles as the CORPUS sees them — `### C1` under a `## Cycles` parent is a real cycle
123
+ # even though check #2's `^## C#` scan (unchanged) doesn't count it.
124
+ pcyc=$(grep -cE '^#+[[:space:]]*C[0-9]+([[:space:]]|$)' "$todo" || true)
125
+ if [ "$pndel" -gt 0 ] && [ "$pcyc" -eq 0 ]; then
126
+ echo "HIGH: promise-coverage CANNOT RUN — $promise has $pndel deliverables but no 'C<n>' cycle section parsed in $todo"
127
+ high=$((high+1)); pndel=0
128
+ elif [ "$pndel" -gt 0 ]; then
129
+ while IFS=$'\t' read -r ac item; do
130
+ [ -n "$ac" ] || continue
131
+ if printf '%s\n' "$corpus" | awk -v s="$ac" '
132
+ { p=index($0,s); while (p>0) {
133
+ c=substr($0,p+length(s),1)
134
+ if (c=="" || c !~ /[A-Za-z0-9_.\/-]/) { found=1; exit }
135
+ rest=substr($0,p+1); q=index(rest,s); if (q==0) break; p=p+q
136
+ } }
137
+ END { exit !found }
138
+ '; then
139
+ pcov=$((pcov+1))
140
+ else
141
+ puncov=$((puncov+1))
142
+ lvl="HIGH"; [ "$strict" -eq 1 ] && lvl="CRITICAL"
143
+ echo "$lvl: promise deliverable has NO cycle — accept: $ac"
144
+ [ -n "${item:-}" ] && echo " item: $item"
145
+ if [ "$strict" -eq 1 ]; then crit=1; else high=$((high+1)); fi
146
+ fi
147
+ done <<< "$accepts"
148
+ fi
149
+ fi
150
+
49
151
  echo "----"
50
- echo "coverage: $ndel deliverables / $ncyc cycles · cycles-without-deliverable=$nodeliv · cycles-without-test=$notest · high=$high"
152
+ echo "coverage: $ndel deliverables / $ncyc cycles · cycles-without-deliverable=$nodeliv · cycles-without-wave=$nowave · cycles-without-test=$notest · high=$high"
153
+ if [ "$pndel" -gt 0 ]; then
154
+ echo "promise-coverage: $pndel promised / $pcov covered / $puncov uncovered · promise=$promise$([ "$strict" -eq 1 ] && echo ' · strict')"
155
+ fi
51
156
  if [ "$crit" -ne 0 ]; then echo "ANALYZE: CRITICAL — fix coverage before build"; exit 1; fi
52
157
  echo "ANALYZE: pass (every cycle ships + is testable)"; exit 0