@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.
- package/agents/w1-recon.md +9 -4
- package/agents/w2-decide.md +51 -28
- package/agents/w3-edit.md +70 -78
- package/agents/w4-verify.md +164 -69
- package/commands/browser.md +98 -35
- package/commands/cc-connect.md +6 -0
- package/commands/chat.md +13 -1
- package/commands/close.md +45 -19
- package/commands/create.md +20 -10
- package/commands/db-sync.md +5 -1
- package/commands/deploy.md +174 -87
- package/commands/do-autonomous.md +31 -20
- package/commands/do-improve.md +4 -4
- package/commands/do-show.md +4 -3
- package/commands/do.md +53 -13
- package/commands/go.md +75 -0
- package/commands/improve.md +4 -2
- package/commands/kill.md +6 -8
- package/commands/notify.md +15 -2
- package/commands/oo-push.md +24 -9
- package/commands/rag.md +6 -0
- package/commands/release.md +46 -25
- package/commands/restart.md +5 -4
- package/commands/see.md +45 -35
- package/commands/skill-create.md +38 -5
- package/commands/sync.md +21 -11
- package/hooks/scripts/auto-continue.sh +70 -0
- package/hooks/scripts/branch-pin.sh +85 -0
- package/hooks/scripts/git-add-guard.sh +16 -5
- package/hooks/scripts/post-edit-check.sh +16 -0
- package/hooks/scripts/session-start.sh +13 -8
- package/hooks/scripts/sync-priority-todo.sh +5 -0
- package/package.json +1 -1
- package/rules/api.md +9 -0
- package/rules/design.md +13 -3
- package/rules/documentation.md +30 -71
- package/rules/engine.md +8 -1
- package/rules/ui.md +5 -1
- package/scripts/__pycache__/do-rank.cpython-313.pyc +0 -0
- package/scripts/browser-check.mjs +44 -103
- package/scripts/cc-events-proof.sh +13 -12
- package/scripts/chrome.mjs +381 -0
- package/scripts/do-analyze.sh +109 -4
- package/scripts/do-auto.sh +350 -43
- package/scripts/do-consumer-sweep.sh +72 -0
- package/scripts/do-derives-check.sh +88 -0
- package/scripts/do-fleet.sh +50 -1
- package/scripts/do-killswitch-audit.py +33 -14
- package/scripts/do-promise-lint.sh +150 -4
- package/scripts/do-promise-settle.sh +131 -9
- package/scripts/do-prove.sh +197 -17
- package/scripts/do-rank.py +606 -20
- package/scripts/do-rubric.py +21 -2
- package/scripts/do-signal.sh +157 -17
- package/scripts/do-smoke.sh +9 -0
- package/scripts/do-substrate-check.sh +49 -0
- package/scripts/do-tasks-wire-check.sh +81 -0
- package/scripts/do-tier.sh +90 -5
- package/scripts/do-ui-gate.sh +332 -0
- package/scripts/do-untracked-gate.sh +231 -0
- package/scripts/do-walk.sh +344 -0
- package/scripts/factory-check.sh +747 -0
- package/scripts/factory-repo.sh +1361 -0
- package/scripts/factory-tasks-check.sh +85 -0
- package/scripts/outcome-pull.ts +279 -0
- package/scripts/promise-manifest.mjs +167 -0
- package/scripts/reconcile-allow.txt +5 -0
- package/scripts/rubric-weights.json +64 -0
- package/scripts/spine-canary.sh +20 -0
- package/scripts/typedb-cluster-status.sh +128 -0
- package/scripts/typedb-env.sh +182 -0
- package/scripts/typedb-flap-recorder.sh +85 -0
- package/scripts/typedb-probes/containment-probe.py +88 -0
- package/scripts/typedb-probes/panic-probe-version.py +92 -0
- package/scripts/typedb-probes/panic-probe.py +77 -0
- package/scripts/typedb-scratch.sh +73 -0
- package/scripts/urls-lint.sh +58 -0
- package/scripts/w4-rubric.ts +96 -12
- package/scripts/wf-check.mjs +75 -0
- package/skills/ai-ui/SKILL.md +1 -1
- package/skills/astro/SKILL.md +177 -132
- package/skills/astro/add-content-collection.md +29 -13
- package/skills/astro/check-build.md +19 -13
- package/skills/astro/create-component.md +36 -9
- package/skills/astro/create-page.md +39 -10
- package/skills/astro/optimize-performance.md +36 -8
- package/skills/cli/SKILL.md +254 -220
- package/skills/dev/SKILL.md +1 -1
- package/skills/directory-autofill/SKILL.md +68 -0
- package/skills/docs/SKILL.md +4 -4
- package/skills/mcp/SKILL.md +186 -156
- package/skills/promise-make/SKILL.md +2 -2
- package/skills/puck/SKILL.md +465 -250
- package/skills/react19/SKILL.md +196 -296
- package/skills/reactflow/SKILL.md +387 -412
- package/skills/sdk/SKILL.md +167 -106
- package/skills/shadcn/SKILL.md +245 -201
- package/skills/signal/SKILL.md +5 -0
- package/skills/sui/SKILL.md +6 -1
- package/skills/tutorial/SKILL.md +15 -0
- package/skills/typedb/SKILL.md +427 -217
- package/skills/typedb/examples/python-patterns.py +11 -4
- package/skills/typedb/examples/query-patterns.tql +8 -3
- package/skills/typedb/examples/schema-patterns.tql +4 -2
- package/skills/typedb/reference/migration-2x-3x.md +19 -1
- package/skills/typedb/reference/python-driver.md +17 -1
- package/skills/voice/SKILL.md +117 -0
- package/skills/writer/SKILL.md +29 -12
- package/templates/template-feature.md +26 -1
- package/templates/template-tests.md +4 -0
- 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))
|
package/scripts/do-analyze.sh
CHANGED
|
@@ -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="
|
|
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
|