deepflow 0.1.87 → 0.1.89
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/bin/install.js +73 -7
- package/hooks/df-dashboard-push.js +170 -0
- package/hooks/df-execution-history.js +120 -0
- package/hooks/df-invariant-check.js +126 -0
- package/hooks/df-spec-lint.js +78 -4
- package/hooks/df-statusline.js +77 -5
- package/hooks/df-tool-usage-spike.js +41 -0
- package/hooks/df-tool-usage.js +86 -0
- package/hooks/df-worktree-guard.js +101 -0
- package/package.json +1 -1
- package/src/commands/df/auto-cycle.md +75 -558
- package/src/commands/df/auto.md +9 -48
- package/src/commands/df/consolidate.md +14 -38
- package/src/commands/df/dashboard.md +35 -0
- package/src/commands/df/debate.md +27 -156
- package/src/commands/df/discover.md +35 -181
- package/src/commands/df/execute.md +283 -563
- package/src/commands/df/note.md +37 -176
- package/src/commands/df/plan.md +80 -210
- package/src/commands/df/report.md +29 -184
- package/src/commands/df/resume.md +18 -101
- package/src/commands/df/spec.md +49 -145
- package/src/commands/df/verify.md +59 -606
- package/src/skills/browse-fetch/SKILL.md +32 -257
- package/src/skills/browse-verify/SKILL.md +40 -174
- package/src/skills/code-completeness/SKILL.md +2 -9
- package/src/skills/gap-discovery/SKILL.md +19 -86
- package/templates/config-template.yaml +10 -0
- package/templates/spec-template.md +12 -1
|
@@ -7,229 +7,35 @@ allowed-tools: [Bash, WebFetch, WebSearch, Read]
|
|
|
7
7
|
|
|
8
8
|
# Browse-Fetch
|
|
9
9
|
|
|
10
|
-
Retrieve live web content with a headless browser. Handles
|
|
10
|
+
Retrieve live web content with a headless browser. Handles JS-rendered pages, SPAs, and dynamic content that WebFetch cannot reach.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- Reading documentation sites that require JavaScript to render (e.g., React-based docs, Vite, Next.js portals)
|
|
15
|
-
- Fetching the current content of a specific URL provided by the user
|
|
16
|
-
- Extracting article or reference content from a known page before implementing code against it
|
|
17
|
-
|
|
18
|
-
## Skip When
|
|
19
|
-
|
|
20
|
-
- The URL is a plain HTML page or GitHub raw file — use WebFetch instead (faster, no overhead)
|
|
21
|
-
- The target requires authentication (login wall) or CAPTCHA — browser cannot bypass; note the block and continue
|
|
12
|
+
**Use when:** URL requires JavaScript rendering (React-based docs, SPAs, portals).
|
|
13
|
+
**Skip when:** Plain HTML / GitHub raw file (use WebFetch) or auth-walled / CAPTCHA-blocked page.
|
|
22
14
|
|
|
23
15
|
---
|
|
24
16
|
|
|
25
17
|
## Browser Core Protocol
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
The fetch script below implements all steps. Summary of what each phase does:
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
1. **Runtime detection** — prefer `node`, fall back to `bun`.
|
|
22
|
+
2. **Install check** — `require('playwright')` or auto-install chromium via npx.
|
|
23
|
+
3. **Launch** — headless Chromium with desktop Chrome user-agent.
|
|
24
|
+
4. **Navigate** — `goto` with `domcontentloaded` (upgrade to `networkidle` if content missing), 30s timeout, 1.5s settle delay.
|
|
25
|
+
5. **Block detection** — check title/URL for login walls, body for CAPTCHA. On detection: log, skip, continue task.
|
|
26
|
+
6. **Extract** — `page.evaluate()` converts DOM → Markdown in-browser (zero deps). Strips noise elements (nav/footer/header/aside/scripts/cookies), picks `main|article|[role=main]|body`, recursive `md()` handles headings, lists, tables, code blocks, links, images, blockquotes, definition lists. Falls back to `page.innerText('body')` if extraction < 100 chars.
|
|
27
|
+
7. **Truncate** — cap at 16000 chars (~4000 tokens). Append `[content truncated]` marker.
|
|
28
|
+
8. **Cleanup** — always `browser.close()` in `finally`.
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
## Fetch Script
|
|
31
|
+
|
|
32
|
+
Inline via `$RUNTIME -e`. Adapt URL per query.
|
|
32
33
|
|
|
33
34
|
```bash
|
|
34
|
-
# Prefer Node.js; fall back to Bun
|
|
35
35
|
if which node > /dev/null 2>&1; then RUNTIME=node; elif which bun > /dev/null 2>&1; then RUNTIME=bun; else echo "Error: neither node nor bun found" && exit 1; fi
|
|
36
36
|
|
|
37
37
|
$RUNTIME -e "require('playwright')" 2>/dev/null \
|
|
38
38
|
|| npx --yes playwright install chromium --with-deps 2>&1 | tail -5
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
If installation fails, fall back to WebFetch (see Fallback section below).
|
|
42
|
-
|
|
43
|
-
### 2. Launch Command
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
# Detect runtime — prefer Node.js per decision
|
|
47
|
-
if which node > /dev/null 2>&1; then RUNTIME=node; elif which bun > /dev/null 2>&1; then RUNTIME=bun; else echo "Error: neither node nor bun found" && exit 1; fi
|
|
48
|
-
|
|
49
|
-
$RUNTIME -e "
|
|
50
|
-
const { chromium } = require('playwright');
|
|
51
|
-
(async () => {
|
|
52
|
-
const browser = await chromium.launch({ headless: true });
|
|
53
|
-
const context = await browser.newContext({
|
|
54
|
-
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
55
|
-
});
|
|
56
|
-
const page = await context.newPage();
|
|
57
|
-
|
|
58
|
-
// --- navigation + extraction (see sections 3–4) ---
|
|
59
|
-
|
|
60
|
-
await browser.close();
|
|
61
|
-
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
62
|
-
"
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 3. Navigation
|
|
66
|
-
|
|
67
|
-
```js
|
|
68
|
-
// Inside the async IIFE above
|
|
69
|
-
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
70
|
-
// Allow JS to settle
|
|
71
|
-
await page.waitForTimeout(1500);
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
- Use `waitUntil: 'domcontentloaded'` for speed; upgrade to `'networkidle'` only if content is missing.
|
|
75
|
-
- Set `timeout: 30000` (30 s). On timeout, treat as graceful failure (see section 5).
|
|
76
|
-
|
|
77
|
-
### 4. Content Extraction
|
|
78
|
-
|
|
79
|
-
Extract content as **structured Markdown** optimized for LLM consumption (not raw HTML or flat text).
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
// Convert DOM to Markdown inside the browser context — zero dependencies
|
|
83
|
-
let text = await page.evaluate(() => {
|
|
84
|
-
// Remove noise elements
|
|
85
|
-
const noise = 'nav, footer, header, aside, script, style, noscript, svg, [role="navigation"], [role="banner"], [role="contentinfo"], .cookie-banner, #cookie-consent';
|
|
86
|
-
document.querySelectorAll(noise).forEach(el => el.remove());
|
|
87
|
-
|
|
88
|
-
// Pick main content container
|
|
89
|
-
const root = document.querySelector('main, article, [role="main"]') || document.body;
|
|
90
|
-
|
|
91
|
-
function md(node, listDepth = 0) {
|
|
92
|
-
if (node.nodeType === 3) return node.textContent;
|
|
93
|
-
if (node.nodeType !== 1) return '';
|
|
94
|
-
const tag = node.tagName.toLowerCase();
|
|
95
|
-
const children = () => Array.from(node.childNodes).map(c => md(c, listDepth)).join('');
|
|
96
|
-
|
|
97
|
-
// Skip hidden elements
|
|
98
|
-
if (node.getAttribute('aria-hidden') === 'true' || node.hidden) return '';
|
|
99
|
-
|
|
100
|
-
switch (tag) {
|
|
101
|
-
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': {
|
|
102
|
-
const level = '#'.repeat(parseInt(tag[1]));
|
|
103
|
-
const text = node.textContent.trim();
|
|
104
|
-
return text ? '\n\n' + level + ' ' + text + '\n\n' : '';
|
|
105
|
-
}
|
|
106
|
-
case 'p': return '\n\n' + children().trim() + '\n\n';
|
|
107
|
-
case 'br': return '\n';
|
|
108
|
-
case 'hr': return '\n\n---\n\n';
|
|
109
|
-
case 'strong': case 'b': { const t = children().trim(); return t ? '**' + t + '**' : ''; }
|
|
110
|
-
case 'em': case 'i': { const t = children().trim(); return t ? '*' + t + '*' : ''; }
|
|
111
|
-
case 'code': {
|
|
112
|
-
const t = node.textContent;
|
|
113
|
-
return node.parentElement && node.parentElement.tagName.toLowerCase() === 'pre' ? t : '`' + t + '`';
|
|
114
|
-
}
|
|
115
|
-
case 'pre': {
|
|
116
|
-
const code = node.querySelector('code');
|
|
117
|
-
const lang = code ? (code.className.match(/language-(\w+)/)||[])[1] || '' : '';
|
|
118
|
-
const t = (code || node).textContent.trim();
|
|
119
|
-
return '\n\n```' + lang + '\n' + t + '\n```\n\n';
|
|
120
|
-
}
|
|
121
|
-
case 'a': {
|
|
122
|
-
const href = node.getAttribute('href');
|
|
123
|
-
const t = children().trim();
|
|
124
|
-
return (href && t && !href.startsWith('#')) ? '[' + t + '](' + href + ')' : t;
|
|
125
|
-
}
|
|
126
|
-
case 'img': {
|
|
127
|
-
const alt = node.getAttribute('alt') || '';
|
|
128
|
-
return alt ? '[image: ' + alt + ']' : '';
|
|
129
|
-
}
|
|
130
|
-
case 'ul': case 'ol': return '\n\n' + children() + '\n';
|
|
131
|
-
case 'li': {
|
|
132
|
-
const indent = ' '.repeat(listDepth);
|
|
133
|
-
const bullet = node.parentElement && node.parentElement.tagName.toLowerCase() === 'ol'
|
|
134
|
-
? (Array.from(node.parentElement.children).indexOf(node) + 1) + '. '
|
|
135
|
-
: '- ';
|
|
136
|
-
const content = Array.from(node.childNodes).map(c => {
|
|
137
|
-
const t = c.tagName && (c.tagName.toLowerCase() === 'ul' || c.tagName.toLowerCase() === 'ol')
|
|
138
|
-
? md(c, listDepth + 1) : md(c, listDepth);
|
|
139
|
-
return t;
|
|
140
|
-
}).join('').trim();
|
|
141
|
-
return indent + bullet + content + '\n';
|
|
142
|
-
}
|
|
143
|
-
case 'table': {
|
|
144
|
-
const rows = Array.from(node.querySelectorAll('tr'));
|
|
145
|
-
if (!rows.length) return '';
|
|
146
|
-
const matrix = rows.map(r => Array.from(r.querySelectorAll('th, td')).map(c => c.textContent.trim()));
|
|
147
|
-
const cols = Math.max(...matrix.map(r => r.length));
|
|
148
|
-
const widths = Array.from({length: cols}, (_, i) => Math.max(...matrix.map(r => (r[i]||'').length), 3));
|
|
149
|
-
let out = '\n\n';
|
|
150
|
-
matrix.forEach((row, ri) => {
|
|
151
|
-
out += '| ' + Array.from({length: cols}, (_, i) => (row[i]||'').padEnd(widths[i])).join(' | ') + ' |\n';
|
|
152
|
-
if (ri === 0) out += '| ' + widths.map(w => '-'.repeat(w)).join(' | ') + ' |\n';
|
|
153
|
-
});
|
|
154
|
-
return out + '\n';
|
|
155
|
-
}
|
|
156
|
-
case 'blockquote': return '\n\n> ' + children().trim().replace(/\n/g, '\n> ') + '\n\n';
|
|
157
|
-
case 'dl': return '\n\n' + children() + '\n';
|
|
158
|
-
case 'dt': return '**' + children().trim() + '**\n';
|
|
159
|
-
case 'dd': return ': ' + children().trim() + '\n';
|
|
160
|
-
case 'div': case 'section': case 'span': case 'figure': case 'figcaption':
|
|
161
|
-
return children();
|
|
162
|
-
default: return children();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let result = md(root);
|
|
167
|
-
// Collapse excessive whitespace
|
|
168
|
-
result = result.replace(/\n{3,}/g, '\n\n').trim();
|
|
169
|
-
return result;
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Fallback if extraction is too short
|
|
173
|
-
if (!text || text.trim().length < 100) {
|
|
174
|
-
text = await page.innerText('body').catch(() => '');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Truncate to ~4000 tokens (~16000 chars) to stay within context budget
|
|
178
|
-
const MAX_CHARS = 16000;
|
|
179
|
-
if (text.length > MAX_CHARS) {
|
|
180
|
-
text = text.slice(0, MAX_CHARS) + '\n\n[content truncated — use a more specific selector or paginate]';
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
console.log(text);
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
For interactive element inspection (e.g., browse-verify), use `locator.ariaSnapshot()` instead of `innerText`.
|
|
187
|
-
|
|
188
|
-
### 5. Graceful Failure
|
|
189
|
-
|
|
190
|
-
Detect and handle blocks without crashing:
|
|
191
|
-
|
|
192
|
-
```js
|
|
193
|
-
const title = await page.title();
|
|
194
|
-
const url = page.url();
|
|
195
|
-
|
|
196
|
-
// Login wall
|
|
197
|
-
if (/sign.?in|log.?in|auth/i.test(title) || url.includes('/login')) {
|
|
198
|
-
console.log(`[browse-fetch] Blocked by login wall at ${url}. Skipping.`);
|
|
199
|
-
await browser.close();
|
|
200
|
-
process.exit(0);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// CAPTCHA
|
|
204
|
-
const bodyText = await page.innerText('body').catch(() => '');
|
|
205
|
-
if (/captcha|robot|human verification/i.test(bodyText)) {
|
|
206
|
-
console.log(`[browse-fetch] CAPTCHA detected at ${url}. Skipping.`);
|
|
207
|
-
await browser.close();
|
|
208
|
-
process.exit(0);
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
On graceful failure: return the URL and a short explanation, then continue with the task using available context.
|
|
213
|
-
|
|
214
|
-
### 6. Cleanup
|
|
215
|
-
|
|
216
|
-
Always close the browser in a `finally` block or after use:
|
|
217
|
-
|
|
218
|
-
```js
|
|
219
|
-
await browser.close();
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
## Fetch Workflow
|
|
225
|
-
|
|
226
|
-
**Goal:** retrieve and return structured Markdown content of a single URL.
|
|
227
|
-
|
|
228
|
-
The full inline script uses `page.evaluate()` to convert DOM → Markdown inside the browser (zero Node dependencies). Adapt the URL per query.
|
|
229
|
-
|
|
230
|
-
```bash
|
|
231
|
-
# Full inline script — adapt URL per query
|
|
232
|
-
if which node > /dev/null 2>&1; then RUNTIME=node; elif which bun > /dev/null 2>&1; then RUNTIME=bun; else echo "Error: neither node nor bun found" && exit 1; fi
|
|
233
39
|
|
|
234
40
|
$RUNTIME -e "
|
|
235
41
|
const { chromium } = require('playwright');
|
|
@@ -342,77 +148,46 @@ const { chromium } = require('playwright');
|
|
|
342
148
|
"
|
|
343
149
|
```
|
|
344
150
|
|
|
345
|
-
|
|
151
|
+
For interactive element inspection (e.g., browse-verify), use `locator.ariaSnapshot()` instead of DOM extraction.
|
|
346
152
|
|
|
347
153
|
---
|
|
348
154
|
|
|
349
155
|
## Search + Navigation Protocol
|
|
350
156
|
|
|
351
|
-
**Time-box:**
|
|
352
|
-
|
|
353
|
-
> Search engines (Google, DuckDuckGo) block headless browsers with CAPTCHAs. Do NOT use Playwright to search them.
|
|
354
|
-
|
|
355
|
-
Instead, use one of these strategies:
|
|
157
|
+
**Time-box:** 60s. **Page cap:** 5 pages per query.
|
|
356
158
|
|
|
357
|
-
|
|
358
|
-
|----------|-------------|
|
|
359
|
-
| Direct URL construction | You know the domain (e.g., `docs.stripe.com/api/charges`) |
|
|
360
|
-
| WebSearch tool | General keyword search before fetching pages |
|
|
361
|
-
| Site-specific search | Navigate to `site.com/search?q=term` if the site exposes it |
|
|
159
|
+
Do NOT use Playwright for Google/DuckDuckGo (CAPTCHA). Instead:
|
|
362
160
|
|
|
363
|
-
|
|
161
|
+
| Strategy | When |
|
|
162
|
+
|----------|------|
|
|
163
|
+
| Direct URL construction | Domain is known (e.g., `docs.stripe.com/api/charges`) |
|
|
164
|
+
| WebSearch tool | General keyword search before fetching |
|
|
165
|
+
| Site-specific search | Site exposes `/search?q=term` |
|
|
364
166
|
|
|
365
|
-
|
|
366
|
-
2. Run the fetch workflow above.
|
|
367
|
-
3. If the page lacks the needed information, look for a next-page link or a more specific sub-URL.
|
|
368
|
-
4. Repeat up to 4 more times (5 total).
|
|
369
|
-
5. Stop and summarize what was found within the 60 s window.
|
|
167
|
+
Navigation loop: construct URL → fetch → if info missing, find next-page or sub-URL → repeat (max 5 pages) → summarize within 60s.
|
|
370
168
|
|
|
371
169
|
---
|
|
372
170
|
|
|
373
171
|
## Session Cache
|
|
374
172
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
For extractions larger than ~4000 tokens, write to a temp file and reference it:
|
|
378
|
-
|
|
379
|
-
```bash
|
|
380
|
-
# Write large extraction to temp file
|
|
381
|
-
TMPFILE=$(mktemp /tmp/browse-fetch-XXXXXX.txt)
|
|
382
|
-
$RUNTIME -e "...script..." > "$TMPFILE"
|
|
383
|
-
echo "Content saved to $TMPFILE"
|
|
384
|
-
# Read relevant sections with grep or head rather than loading all at once
|
|
385
|
-
```
|
|
173
|
+
Context window is the cache. For extractions > ~4000 tokens, write to temp file (`mktemp /tmp/browse-fetch-XXXXXX.txt`) and read relevant sections with grep/head.
|
|
386
174
|
|
|
387
175
|
---
|
|
388
176
|
|
|
389
177
|
## Fallback Without Playwright
|
|
390
178
|
|
|
391
|
-
When Playwright is unavailable or fails to install, fall back to the WebFetch tool for:
|
|
392
|
-
|
|
393
|
-
- Static HTML sites (GitHub README, raw docs, Wikipedia)
|
|
394
|
-
- Any URL the user provides where JavaScript rendering is not required
|
|
395
|
-
|
|
396
179
|
| Condition | Action |
|
|
397
180
|
|-----------|--------|
|
|
398
|
-
|
|
|
399
|
-
|
|
|
400
|
-
| Playwright times out twice | Use WebFetch as fallback
|
|
401
|
-
|
|
402
|
-
```
|
|
403
|
-
WebFetch: { url: "https://example.com/page", prompt: "Extract the main content" }
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
If WebFetch also fails, return the URL with an explanation and continue the task.
|
|
181
|
+
| Playwright install fails | Use WebFetch |
|
|
182
|
+
| Known static domain (github raw, pastebin, wikipedia) | Use WebFetch directly, skip Playwright |
|
|
183
|
+
| Playwright times out twice | Use WebFetch as fallback |
|
|
184
|
+
| WebFetch also fails | Return URL with explanation, continue task |
|
|
407
185
|
|
|
408
186
|
---
|
|
409
187
|
|
|
410
188
|
## Rules
|
|
411
189
|
|
|
412
|
-
-
|
|
413
|
-
-
|
|
414
|
-
-
|
|
415
|
-
- Truncate output at ~4000 tokens (~16 000 chars) to protect context budget.
|
|
416
|
-
- On login wall or CAPTCHA, log the block, skip, and continue — never retry infinitely.
|
|
417
|
-
- Close the browser in every code path (use `finally`).
|
|
190
|
+
- Never navigate to Google/DuckDuckGo with Playwright — use WebSearch or direct URLs.
|
|
191
|
+
- On login wall or CAPTCHA: log, skip, continue. Never retry infinitely.
|
|
192
|
+
- Close browser in every code path (`finally` block).
|
|
418
193
|
- Do not persist browser sessions across unrelated tasks.
|
|
@@ -8,49 +8,18 @@ context: fork
|
|
|
8
8
|
|
|
9
9
|
Headless browser verification using Playwright's accessibility tree. Evaluates structured assertions from PLAN.md without LLM calls — purely deterministic matching.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**Use when:** Spec has browser-based ACs (element presence, text content, roles, interactive states, structure).
|
|
12
|
+
**Skip when:** No browser-facing ACs or backend-only implementation.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
- Visual/layout checks (element presence, text content, roles)
|
|
15
|
-
- Interactive state checks (aria-checked, aria-expanded, aria-disabled)
|
|
16
|
-
- Structural checks (element within a container)
|
|
14
|
+
**Prerequisites:** Node.js or Bun + Playwright 1.x. Runtime detection and browser auto-install follow the same protocol as browse-fetch (`which node || which bun`; `npx playwright install chromium`).
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Prerequisites
|
|
21
|
-
|
|
22
|
-
- Node.js (preferred) or Bun
|
|
23
|
-
- Playwright 1.x (`npm install playwright` or `npx playwright install`)
|
|
24
|
-
- Chromium browser (auto-installed if missing)
|
|
25
|
-
|
|
26
|
-
## Runtime Detection
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
# Prefer Node.js; fall back to Bun
|
|
30
|
-
if which node > /dev/null 2>&1; then
|
|
31
|
-
RUNTIME=node
|
|
32
|
-
elif which bun > /dev/null 2>&1; then
|
|
33
|
-
RUNTIME=bun
|
|
34
|
-
else
|
|
35
|
-
echo "Error: neither node nor bun found" && exit 1
|
|
36
|
-
fi
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Browser Auto-Install
|
|
40
|
-
|
|
41
|
-
Before running, ensure Chromium is available:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npx playwright install chromium
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Run this once per environment. If it fails due to permissions, instruct the user to run it manually.
|
|
16
|
+
---
|
|
48
17
|
|
|
49
18
|
## Protocol
|
|
50
19
|
|
|
51
20
|
### 1. Read Assertions from PLAN.md
|
|
52
21
|
|
|
53
|
-
Assertions are written
|
|
22
|
+
Assertions are written by the `plan` skill. Format:
|
|
54
23
|
|
|
55
24
|
```yaml
|
|
56
25
|
assertions:
|
|
@@ -65,10 +34,6 @@ assertions:
|
|
|
65
34
|
name: "Dashboard"
|
|
66
35
|
check: visible
|
|
67
36
|
within: main
|
|
68
|
-
- role: textbox
|
|
69
|
-
name: "Email"
|
|
70
|
-
check: value
|
|
71
|
-
value: "user@example.com"
|
|
72
37
|
```
|
|
73
38
|
|
|
74
39
|
Assertion schema:
|
|
@@ -78,36 +43,29 @@ Assertion schema:
|
|
|
78
43
|
| `role` | yes | ARIA role (button, checkbox, heading, textbox, link, etc.) |
|
|
79
44
|
| `name` | yes | Accessible name (exact or partial match) |
|
|
80
45
|
| `check` | yes | One of: `visible`, `absent`, `state`, `value`, `count` |
|
|
81
|
-
| `value` | no | Expected value for `state` or `
|
|
46
|
+
| `value` | no | Expected value for `state`, `value`, or `count` checks |
|
|
82
47
|
| `within` | no | Ancestor role or selector to scope the search |
|
|
83
48
|
|
|
84
49
|
### 2. Launch Browser and Navigate
|
|
85
50
|
|
|
86
51
|
```javascript
|
|
87
|
-
const { chromium } = require('playwright');
|
|
88
|
-
|
|
89
52
|
const browser = await chromium.launch({ headless: true });
|
|
90
53
|
const page = await browser.newPage();
|
|
91
|
-
|
|
92
54
|
await page.goto(TARGET_URL, { waitUntil: 'networkidle' });
|
|
93
55
|
```
|
|
94
56
|
|
|
95
|
-
`TARGET_URL`
|
|
57
|
+
`TARGET_URL` from spec metadata or passed as argument.
|
|
96
58
|
|
|
97
59
|
### 3. Extract Accessibility Tree
|
|
98
60
|
|
|
99
61
|
Use `locator.ariaSnapshot()` — **NOT** `page.accessibility.snapshot()` (removed in Playwright 1.x):
|
|
100
62
|
|
|
101
63
|
```javascript
|
|
102
|
-
// Full-page aria snapshot (YAML-like role tree)
|
|
103
64
|
const snapshot = await page.locator('body').ariaSnapshot();
|
|
104
|
-
|
|
105
|
-
// Scoped snapshot within a container
|
|
106
|
-
const containerSnapshot = await page.locator('main').ariaSnapshot();
|
|
65
|
+
// Scoped: await page.locator('main').ariaSnapshot();
|
|
107
66
|
```
|
|
108
67
|
|
|
109
|
-
|
|
110
|
-
|
|
68
|
+
Returns YAML-like role tree:
|
|
111
69
|
```yaml
|
|
112
70
|
- heading "Dashboard" [level=1]
|
|
113
71
|
- button "Submit" [disabled]
|
|
@@ -115,151 +73,59 @@ const containerSnapshot = await page.locator('main').ariaSnapshot();
|
|
|
115
73
|
- textbox "Email": user@example.com
|
|
116
74
|
```
|
|
117
75
|
|
|
118
|
-
### 4.
|
|
76
|
+
### 4. Bounding Boxes (optional)
|
|
119
77
|
|
|
120
|
-
For spatial/layout assertions or
|
|
78
|
+
For spatial/layout assertions: `await page.getByRole(role, { name }).boundingBox()` returns `{ x, y, width, height }` or null.
|
|
121
79
|
|
|
122
|
-
|
|
123
|
-
const element = page.getByRole(role, { name: assertionName });
|
|
124
|
-
const box = await element.boundingBox();
|
|
125
|
-
// box: { x, y, width, height } or null if not visible
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 5. Evaluate Assertions Deterministically
|
|
80
|
+
### 5. Evaluate Assertions
|
|
129
81
|
|
|
130
|
-
Parse the aria snapshot and evaluate each assertion
|
|
131
|
-
|
|
132
|
-
```javascript
|
|
133
|
-
function evaluateAssertion(snapshot, assertion) {
|
|
134
|
-
const { role, name, check, value, within } = assertion;
|
|
82
|
+
Parse the aria snapshot and evaluate each assertion deterministically (no LLM calls).
|
|
135
83
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return treeContains(tree, role, name);
|
|
144
|
-
|
|
145
|
-
case 'absent':
|
|
146
|
-
return !treeContains(tree, role, name);
|
|
147
|
-
|
|
148
|
-
case 'state':
|
|
149
|
-
// e.g., value: "checked", "disabled", "expanded"
|
|
150
|
-
return treeContainsWithState(tree, role, name, value);
|
|
151
|
-
|
|
152
|
-
case 'value':
|
|
153
|
-
// Matches textbox/combobox displayed value
|
|
154
|
-
return treeContainsWithValue(tree, role, name, value);
|
|
155
|
-
|
|
156
|
-
case 'count':
|
|
157
|
-
return countMatches(tree, role, name) === parseInt(value, 10);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
```
|
|
84
|
+
| Check | Logic |
|
|
85
|
+
|-------|-------|
|
|
86
|
+
| `visible` | Role+name found in tree |
|
|
87
|
+
| `absent` | Role+name NOT found in tree |
|
|
88
|
+
| `state` | Role+name found with state token (e.g., `[checked]`, `[disabled]`, `[expanded]`) |
|
|
89
|
+
| `value` | Role+name found with matching displayed value (textbox/combobox) |
|
|
90
|
+
| `count` | Number of role+name matches equals `parseInt(value)` |
|
|
161
91
|
|
|
162
92
|
Matching rules:
|
|
163
|
-
- Role matching
|
|
164
|
-
- Name matching
|
|
165
|
-
-
|
|
166
|
-
|
|
167
|
-
### 6. Capture Screenshot
|
|
93
|
+
- Role matching: case-insensitive
|
|
94
|
+
- Name matching: case-insensitive substring (exact match only when assertion wraps name in quotes)
|
|
95
|
+
- If `within` specified, scope to that ancestor's subtree first
|
|
168
96
|
|
|
169
|
-
|
|
97
|
+
### 6. Screenshot
|
|
170
98
|
|
|
171
|
-
|
|
172
|
-
const screenshotDir = `.deepflow/screenshots/${specName}`;
|
|
173
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
174
|
-
const screenshotPath = `${screenshotDir}/${timestamp}.png`;
|
|
175
|
-
|
|
176
|
-
await fs.mkdir(screenshotDir, { recursive: true });
|
|
177
|
-
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
99
|
+
Capture after every run regardless of pass/fail:
|
|
178
100
|
```
|
|
179
|
-
|
|
180
|
-
|
|
101
|
+
.deepflow/screenshots/{spec-name}/{ISO-timestamp}.png
|
|
102
|
+
```
|
|
103
|
+
Use `page.screenshot({ path, fullPage: true })`.
|
|
181
104
|
|
|
182
105
|
### 7. Report Results
|
|
183
106
|
|
|
184
|
-
Emit a structured result for each assertion:
|
|
185
|
-
|
|
186
107
|
```
|
|
187
|
-
[PASS] button "Submit" — visible
|
|
188
|
-
[PASS] checkbox "Accept terms" — state: checked ✓
|
|
108
|
+
[PASS] button "Submit" — visible
|
|
189
109
|
[FAIL] heading "Dashboard" — expected visible, not found in snapshot
|
|
190
|
-
[PASS] textbox "Email" — value: user@example.com ✓
|
|
191
110
|
|
|
192
|
-
Results:
|
|
111
|
+
Results: 1 passed, 1 failed
|
|
193
112
|
Screenshot: .deepflow/screenshots/login-form/2026-03-14T12-00-00-000Z.png
|
|
194
113
|
```
|
|
195
114
|
|
|
196
|
-
Exit
|
|
115
|
+
Exit code 0 if all pass, 1 if any fail.
|
|
197
116
|
|
|
198
117
|
### 8. Tear Down
|
|
199
118
|
|
|
200
|
-
|
|
201
|
-
await browser.close();
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
Always close the browser, even on error (use try/finally).
|
|
119
|
+
Always `browser.close()` in a `finally` block.
|
|
205
120
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
```javascript
|
|
209
|
-
#!/usr/bin/env node
|
|
210
|
-
const { chromium } = require('playwright');
|
|
211
|
-
const fs = require('fs/promises');
|
|
212
|
-
const path = require('path');
|
|
213
|
-
|
|
214
|
-
async function main({ targetUrl, specName, assertions }) {
|
|
215
|
-
// Auto-install chromium if needed
|
|
216
|
-
// (handled by: npx playwright install chromium)
|
|
217
|
-
|
|
218
|
-
const browser = await chromium.launch({ headless: true });
|
|
219
|
-
const page = await browser.newPage();
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
await page.goto(targetUrl, { waitUntil: 'networkidle' });
|
|
223
|
-
|
|
224
|
-
const snapshot = await page.locator('body').ariaSnapshot();
|
|
225
|
-
|
|
226
|
-
const results = assertions.map(assertion => ({
|
|
227
|
-
assertion,
|
|
228
|
-
passed: evaluateAssertion(snapshot, assertion),
|
|
229
|
-
}));
|
|
230
|
-
|
|
231
|
-
// Screenshot
|
|
232
|
-
const screenshotDir = path.join('.deepflow', 'screenshots', specName);
|
|
233
|
-
await fs.mkdir(screenshotDir, { recursive: true });
|
|
234
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
235
|
-
const screenshotPath = path.join(screenshotDir, `${timestamp}.png`);
|
|
236
|
-
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
237
|
-
|
|
238
|
-
// Report
|
|
239
|
-
const passed = results.filter(r => r.passed).length;
|
|
240
|
-
const failed = results.filter(r => !r.passed).length;
|
|
241
|
-
|
|
242
|
-
for (const { assertion, passed: ok } of results) {
|
|
243
|
-
const status = ok ? '[PASS]' : '[FAIL]';
|
|
244
|
-
console.log(`${status} ${assertion.role} "${assertion.name}" — ${assertion.check}${assertion.value ? ': ' + assertion.value : ''}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
248
|
-
console.log(`Screenshot: ${screenshotPath}`);
|
|
249
|
-
|
|
250
|
-
process.exit(failed > 0 ? 1 : 0);
|
|
251
|
-
} finally {
|
|
252
|
-
await browser.close();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
```
|
|
121
|
+
---
|
|
256
122
|
|
|
257
123
|
## Rules
|
|
258
124
|
|
|
259
|
-
- Never call an LLM during the verify phase — all
|
|
260
|
-
- Always use `locator.ariaSnapshot()`, never `page.accessibility.snapshot()` (removed)
|
|
261
|
-
- Always close
|
|
262
|
-
- Screenshot every run regardless of
|
|
263
|
-
- If Playwright
|
|
264
|
-
- Partial name matching is
|
|
265
|
-
- Report results
|
|
125
|
+
- Never call an LLM during the verify phase — all evaluation is deterministic.
|
|
126
|
+
- Always use `locator.ariaSnapshot()`, never `page.accessibility.snapshot()` (removed).
|
|
127
|
+
- Always close browser in `finally`.
|
|
128
|
+
- Screenshot every run regardless of outcome.
|
|
129
|
+
- If Playwright not installed, emit clear error with instructions — don't silently skip.
|
|
130
|
+
- Partial name matching is default; exact only when assertion specifies it.
|
|
131
|
+
- Report results in structured format above for downstream parsing.
|
|
@@ -4,9 +4,7 @@ description: Finds incomplete code in codebase. Use when analyzing for TODOs, st
|
|
|
4
4
|
allowed-tools: [Read, Grep, Glob]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Code Completeness
|
|
8
|
-
|
|
9
|
-
Find incomplete work in codebase.
|
|
7
|
+
# Code Completeness — Find Incomplete Work
|
|
10
8
|
|
|
11
9
|
## Explicit Markers
|
|
12
10
|
|
|
@@ -30,8 +28,7 @@ Find incomplete work in codebase.
|
|
|
30
28
|
|
|
31
29
|
| Pattern | Language |
|
|
32
30
|
|---------|----------|
|
|
33
|
-
| `it.skip`, `test.skip` | JavaScript |
|
|
34
|
-
| `describe.skip` | JavaScript |
|
|
31
|
+
| `it.skip`, `test.skip`, `describe.skip` | JavaScript |
|
|
35
32
|
| `@pytest.mark.skip` | Python |
|
|
36
33
|
| `t.Skip()` | Go |
|
|
37
34
|
|
|
@@ -51,10 +48,6 @@ REQ-1: {requirement}
|
|
|
51
48
|
Status: PARTIAL
|
|
52
49
|
Found: src/api/upload.ts:45 — `// TODO: validation`
|
|
53
50
|
Action: Complete validation
|
|
54
|
-
|
|
55
|
-
REQ-2: {requirement}
|
|
56
|
-
Status: MISSING
|
|
57
|
-
Action: Create src/services/storage.ts
|
|
58
51
|
```
|
|
59
52
|
|
|
60
53
|
## Rules
|