feed-the-machine 1.0.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/LICENSE +21 -0
- package/README.md +268 -0
- package/bin/generate-manifest.mjs +210 -0
- package/bin/install.mjs +114 -0
- package/ftm/SKILL.md +88 -0
- package/ftm-audit/SKILL.md +146 -0
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -0
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -0
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -0
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -0
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -0
- package/ftm-audit/scripts/run-knip.sh +23 -0
- package/ftm-audit.yml +2 -0
- package/ftm-brainstorm/SKILL.md +379 -0
- package/ftm-brainstorm/evals/evals.json +100 -0
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -0
- package/ftm-brainstorm/references/agent-prompts.md +224 -0
- package/ftm-brainstorm/references/plan-template.md +121 -0
- package/ftm-brainstorm.yml +2 -0
- package/ftm-browse/SKILL.md +415 -0
- package/ftm-browse/daemon/browser-manager.ts +206 -0
- package/ftm-browse/daemon/bun.lock +30 -0
- package/ftm-browse/daemon/cli.ts +347 -0
- package/ftm-browse/daemon/commands.ts +410 -0
- package/ftm-browse/daemon/main.ts +357 -0
- package/ftm-browse/daemon/package.json +17 -0
- package/ftm-browse/daemon/server.ts +189 -0
- package/ftm-browse/daemon/snapshot.ts +519 -0
- package/ftm-browse/daemon/tsconfig.json +22 -0
- package/ftm-browse.yml +4 -0
- package/ftm-codex-gate/SKILL.md +302 -0
- package/ftm-codex-gate.yml +2 -0
- package/ftm-config/SKILL.md +310 -0
- package/ftm-config.default.yml +80 -0
- package/ftm-config.yml +2 -0
- package/ftm-council/SKILL.md +132 -0
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -0
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -0
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -0
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -0
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -0
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -0
- package/ftm-council.yml +2 -0
- package/ftm-dashboard.yml +4 -0
- package/ftm-debug/SKILL.md +146 -0
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -0
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -0
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -0
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -0
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -0
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -0
- package/ftm-debug.yml +2 -0
- package/ftm-diagram/SKILL.md +233 -0
- package/ftm-diagram.yml +2 -0
- package/ftm-executor/SKILL.md +657 -0
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -0
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -0
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -0
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -0
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -0
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -0
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -0
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -0
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +44 -0
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -0
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -0
- package/ftm-executor/runtime/package.json +8 -0
- package/ftm-executor.yml +2 -0
- package/ftm-git/SKILL.md +195 -0
- package/ftm-git/evals/evals.json +26 -0
- package/ftm-git/evals/promptfoo.yaml +75 -0
- package/ftm-git/hooks/post-commit-experience.sh +92 -0
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -0
- package/ftm-git/references/protocols/REMEDIATION.md +139 -0
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -0
- package/ftm-git.yml +2 -0
- package/ftm-intent/SKILL.md +198 -0
- package/ftm-intent.yml +2 -0
- package/ftm-map.yml +2 -0
- package/ftm-mind/SKILL.md +986 -0
- package/ftm-mind/evals/promptfoo.yaml +142 -0
- package/ftm-mind/references/blackboard-schema.md +328 -0
- package/ftm-mind/references/complexity-guide.md +110 -0
- package/ftm-mind/references/event-registry.md +299 -0
- package/ftm-mind/references/mcp-inventory.md +296 -0
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -0
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -0
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -0
- package/ftm-mind/references/reflexion-protocol.md +249 -0
- package/ftm-mind/references/routing/SCENARIOS.md +22 -0
- package/ftm-mind/references/routing-scenarios.md +35 -0
- package/ftm-mind.yml +2 -0
- package/ftm-pause/SKILL.md +133 -0
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -0
- package/ftm-pause/references/protocols/VALIDATION.md +80 -0
- package/ftm-pause.yml +2 -0
- package/ftm-researcher.yml +2 -0
- package/ftm-resume/SKILL.md +166 -0
- package/ftm-resume/references/protocols/VALIDATION.md +172 -0
- package/ftm-resume.yml +2 -0
- package/ftm-retro/SKILL.md +189 -0
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -0
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -0
- package/ftm-retro.yml +2 -0
- package/ftm-routine.yml +4 -0
- package/ftm-state/blackboard/context.json +23 -0
- package/ftm-state/blackboard/experiences/index.json +9 -0
- package/ftm-state/blackboard/patterns.json +6 -0
- package/ftm-state/schemas/context.schema.json +130 -0
- package/ftm-state/schemas/experience-index.schema.json +77 -0
- package/ftm-state/schemas/experience.schema.json +78 -0
- package/ftm-state/schemas/patterns.schema.json +44 -0
- package/ftm-upgrade/SKILL.md +153 -0
- package/ftm-upgrade/scripts/check-version.sh +76 -0
- package/ftm-upgrade/scripts/upgrade.sh +143 -0
- package/ftm-upgrade.yml +2 -0
- package/ftm.yml +2 -0
- package/install.sh +102 -0
- package/package.json +74 -0
- package/uninstall.sh +25 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ftm-browse
|
|
3
|
+
description: Headless browser daemon for visual verification and web interaction. Gives agents the ability to navigate, screenshot, click, fill forms, and inspect ARIA trees via CLI commands. Use when user says "browse", "screenshot", "visual", "look at the app", "open browser", "check the page", "navigate to", "take a screenshot", "visual verification".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Events
|
|
7
|
+
|
|
8
|
+
### Emits
|
|
9
|
+
- `task_completed` — when a visual verification or interaction workflow finishes successfully
|
|
10
|
+
|
|
11
|
+
### Listens To
|
|
12
|
+
(none — ftm-browse is invoked on demand and does not respond to events)
|
|
13
|
+
|
|
14
|
+
# ftm-browse
|
|
15
|
+
|
|
16
|
+
ftm-browse is a persistent headless Chromium daemon controlled via a CLI binary at `~/.claude/skills/ftm-browse/bin/ftm-browse`. Each CLI invocation communicates with the daemon over a local HTTP server (bearer-auth, random port), so the browser stays alive across commands without the per-invocation startup penalty. The daemon auto-starts on first use and shuts itself down after 30 minutes of idle. This CLI-to-HTTP model is 4x more token-efficient than driving Playwright MCP directly, because tool calls remain terse and outputs are structured JSON rather than raw browser protocol noise.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
**First run — install the browser engine:**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx playwright install chromium
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Define the alias in any shell session before use:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
PB="$HOME/.claude/skills/ftm-browse/bin/ftm-browse"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Verify the installation:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
$PB goto https://example.com && $PB screenshot
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The first `goto` command will start the daemon (up to 10 seconds for cold start). All subsequent commands respond in ~100ms because the browser process stays alive.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Command Reference
|
|
45
|
+
|
|
46
|
+
### WRITE commands — state-mutating
|
|
47
|
+
|
|
48
|
+
These commands change browser state. Never retry blindly; check the returned `success` field.
|
|
49
|
+
|
|
50
|
+
**`goto <url>`** — Navigate to a URL. Waits for `domcontentloaded`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
$PB goto https://example.com
|
|
54
|
+
$PB goto http://localhost:3000/dashboard
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Returns: `{ success, data: { url, title, status } }`
|
|
58
|
+
|
|
59
|
+
**`click <@ref>`** — Click an interactive element by its `@e` ref. Performs a 5ms staleness check before clicking; fails immediately if the ref is stale rather than waiting.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
$PB click @e3
|
|
63
|
+
$PB click @e12
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Returns: `{ success, data: { url, title } }` — includes the URL after any resulting navigation.
|
|
67
|
+
|
|
68
|
+
**`fill <@ref> <value>`** — Fill a text input, textarea, or other fillable element. Values with spaces do not need quoting — remaining CLI args are joined.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
$PB fill @e2 hello world
|
|
72
|
+
$PB fill @e5 user@example.com
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Returns: `{ success, data: { ref, value } }`
|
|
76
|
+
|
|
77
|
+
**`press <key>`** — Send a keyboard key to the active page. Accepts any Playwright key name.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
$PB press Enter
|
|
81
|
+
$PB press Tab
|
|
82
|
+
$PB press Escape
|
|
83
|
+
$PB press ArrowDown
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Returns: `{ success, data: { key, url } }`
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### READ commands — safe to retry
|
|
91
|
+
|
|
92
|
+
These commands do not change browser state. Safe to call multiple times.
|
|
93
|
+
|
|
94
|
+
**`text`** — Get visible page text via `document.body.innerText`.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
$PB text
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Returns: `{ success, data: { text } }`
|
|
101
|
+
|
|
102
|
+
**`html`** — Get full page HTML via `page.content()`.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
$PB html
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Returns: `{ success, data: { html } }`
|
|
109
|
+
|
|
110
|
+
**`eval <js-expression>`** — Execute arbitrary JavaScript in the page context. Returns the result as a JSON-serializable value. DOM elements are returned as `{ type: "element", tagName, id, text }`. Functions are returned as `{ type: "function", name }`. Errors inside the expression are caught and returned as `{ error: "..." }` rather than crashing the daemon.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
$PB eval document.title
|
|
114
|
+
$PB eval "document.querySelector('input[name=email]').value"
|
|
115
|
+
$PB eval "Array.from(document.querySelectorAll('td')).map(td => td.textContent)"
|
|
116
|
+
$PB eval window.location.href
|
|
117
|
+
$PB eval "document.cookie"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Returns: `{ success, data: { result } }` — `result` is whatever the expression evaluated to.
|
|
121
|
+
|
|
122
|
+
Use cases:
|
|
123
|
+
- Reading hidden or programmatically-set form field values not visible in the ARIA tree
|
|
124
|
+
- Extracting data from dynamic tables or rendered lists
|
|
125
|
+
- Checking feature toggle states or global JS variables (`window.featureFlags`)
|
|
126
|
+
- Inspecting `localStorage` or `sessionStorage` values
|
|
127
|
+
- Querying computed DOM state not exposed via accessibility roles
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### META commands
|
|
132
|
+
|
|
133
|
+
**`snapshot`** — Full ARIA accessibility tree of the current page. Includes both interactive and structural elements (headings, nav, main, etc.).
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
$PB snapshot
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Returns: `{ success, data: { url, title, interactive_only: false, tree, refs, aria_text? } }`
|
|
140
|
+
|
|
141
|
+
**`snapshot -i`** — Interactive elements only, each labeled with an `@e1`, `@e2`... ref. Use this before clicking or filling — never guess a ref.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
$PB snapshot -i
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Returns: same shape as `snapshot` with `interactive_only: true`; the `refs` map contains the locator entries for each `@eN`.
|
|
148
|
+
|
|
149
|
+
**`screenshot`** — Capture a viewport screenshot (1280x800). Saves to `~/.ftm-browse/screenshots/screenshot-<timestamp>.png` by default and returns the path.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
$PB screenshot
|
|
153
|
+
$PB screenshot --path /tmp/before.png
|
|
154
|
+
$PB screenshot --path /tmp/after.png
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Returns: `{ success, data: { path, url, title } }`
|
|
158
|
+
|
|
159
|
+
**`tabs`** — List all open browser tabs.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
$PB tabs
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Returns: `{ success, data: { tabs: [{ index, url, title, active }] } }`
|
|
166
|
+
|
|
167
|
+
**`chain '<json-array>'`** — Execute multiple commands in sequence in a single CLI invocation. The chain stops at the first failure. Use this to reduce round-trips for multi-step operations.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
$PB chain '[
|
|
171
|
+
{"command":"goto","args":{"url":"https://example.com"}},
|
|
172
|
+
{"command":"snapshot","args":{"interactive_only":true}},
|
|
173
|
+
{"command":"screenshot","args":{}}
|
|
174
|
+
]'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Returns: `{ success, data: { results: [{ command, result }] } }`. On failure: adds `failed_at` field.
|
|
178
|
+
|
|
179
|
+
**`health`** — Check that the daemon is alive and responding.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
$PB health
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Returns: `{ status: "ok", pid }` (wrapped in standard result envelope from the daemon's health handler — note this endpoint bypasses `executeCommand` and returns directly).
|
|
186
|
+
|
|
187
|
+
**`stop`** (alias: `shutdown`) — Send SIGTERM to the daemon. The daemon cleans up its state file and exits.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
$PB stop
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## The @e Ref System
|
|
196
|
+
|
|
197
|
+
Refs are short handles (`@e1`, `@e2`, ...) that identify interactive elements. They are assigned fresh on each `snapshot` call and map to stable Playwright locator strategies (by label, by role+name, by placeholder, by name attribute, or by nth-position CSS fallback).
|
|
198
|
+
|
|
199
|
+
**Getting refs:**
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
$PB snapshot -i
|
|
203
|
+
# Output includes tree nodes like:
|
|
204
|
+
# { "ref": "@e3", "role": "button", "name": "Submit", "interactive": true }
|
|
205
|
+
# { "ref": "@e5", "role": "textbox", "name": "Email", "interactive": true }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Using refs:**
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
$PB fill @e5 user@example.com
|
|
212
|
+
$PB click @e3
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Staleness rule:** After any navigation event — whether from `goto`, a `click` that follows a link, or `press Enter` submitting a form — the current ref map is invalidated. The daemon detects stale refs in ~5ms and returns an error asking you to re-snapshot. Always re-run `snapshot -i` after navigation before using refs again.
|
|
216
|
+
|
|
217
|
+
**Typical interaction workflow:**
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# 1. Navigate to the page
|
|
221
|
+
$PB goto http://localhost:3000/login
|
|
222
|
+
|
|
223
|
+
# 2. Get interactive refs
|
|
224
|
+
$PB snapshot -i
|
|
225
|
+
|
|
226
|
+
# 3. Identify target elements from the output, then interact
|
|
227
|
+
$PB fill @e2 admin@example.com
|
|
228
|
+
$PB fill @e3 password123
|
|
229
|
+
$PB click @e4 # "Sign in" button
|
|
230
|
+
|
|
231
|
+
# 4. Page navigated — refs are stale; re-snapshot
|
|
232
|
+
$PB snapshot -i
|
|
233
|
+
|
|
234
|
+
# 5. Continue on the new page
|
|
235
|
+
$PB screenshot
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Common Workflows
|
|
241
|
+
|
|
242
|
+
### Visual smoke test
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
PB="$HOME/.claude/skills/ftm-browse/bin/ftm-browse"
|
|
246
|
+
$PB goto http://localhost:3000
|
|
247
|
+
$PB screenshot --path /tmp/smoke.png
|
|
248
|
+
# Read /tmp/smoke.png to verify layout
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Form filling
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
$PB goto http://localhost:3000/signup
|
|
255
|
+
$PB snapshot -i
|
|
256
|
+
# Identify: @e1=name input, @e2=email input, @e3=password, @e4=submit button
|
|
257
|
+
$PB fill @e1 Jane Doe
|
|
258
|
+
$PB fill @e2 jane@example.com
|
|
259
|
+
$PB fill @e3 s3cret!
|
|
260
|
+
$PB click @e4
|
|
261
|
+
$PB screenshot --path /tmp/after-signup.png
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Navigation verification
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
$PB goto http://localhost:3000
|
|
268
|
+
$PB snapshot -i
|
|
269
|
+
# Find nav links
|
|
270
|
+
$PB click @e7 # "Dashboard" link
|
|
271
|
+
$PB text # Verify content changed
|
|
272
|
+
$PB screenshot
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Before/after comparison
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
$PB goto http://localhost:3000/widget
|
|
279
|
+
$PB screenshot --path /tmp/before.png
|
|
280
|
+
# ... make changes in code ...
|
|
281
|
+
$PB goto http://localhost:3000/widget # reload after change
|
|
282
|
+
$PB screenshot --path /tmp/after.png
|
|
283
|
+
# Compare /tmp/before.png and /tmp/after.png visually
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Multi-step with chain (fewer round-trips)
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
$PB chain '[
|
|
290
|
+
{"command":"goto","args":{"url":"http://localhost:3000/login"}},
|
|
291
|
+
{"command":"fill","args":{"ref":"@e2","value":"admin@example.com"}},
|
|
292
|
+
{"command":"fill","args":{"ref":"@e3","value":"password"}},
|
|
293
|
+
{"command":"click","args":{"ref":"@e4"}},
|
|
294
|
+
{"command":"screenshot","args":{}}
|
|
295
|
+
]'
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Note: When using `chain` with refs, you must have called `snapshot -i` first in a separate command to populate the ref map. Refs set by a `snapshot` inside the same chain are available to subsequent steps in that chain.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Integration with Other FTM Skills
|
|
303
|
+
|
|
304
|
+
**ftm-debug** — Use ftm-browse to visually verify bug fixes. Take a screenshot before applying a fix, apply the fix, reload, screenshot again. Compare before/after to confirm the fix is visible. Also use `snapshot` to inspect DOM state when debugging rendering issues — the ARIA tree reveals whether components have mounted and populated correctly.
|
|
305
|
+
|
|
306
|
+
**ftm-audit** — Use ftm-browse to verify runtime wiring. Navigate to each route the audit is checking, call `snapshot` to confirm the component appears in the ARIA tree with the correct role and name, and screenshot for documentation. This catches hydration failures, missing route registrations, and components that render blank.
|
|
307
|
+
|
|
308
|
+
**ftm-executor** — After completing a task that touches frontend code, use ftm-browse as the post-task smoke test harness. If the project has a dev server running, `goto` the affected route, take a screenshot, and verify the page renders without errors. Include the screenshot path in the task completion report.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Supervised Execution Mode
|
|
313
|
+
|
|
314
|
+
When ftm-browse is executing browser steps within an approved plan (dispatched by ftm-executor or ftm-mind), activate supervised mode. This mode adds verification guardrails after every navigation action.
|
|
315
|
+
|
|
316
|
+
### Activation
|
|
317
|
+
|
|
318
|
+
Supervised mode activates automatically when:
|
|
319
|
+
- The browse command is part of a plan step (context includes plan step reference)
|
|
320
|
+
- The caller provides expected page state (title pattern, URL pattern, or element selector)
|
|
321
|
+
|
|
322
|
+
### Post-Navigation Verification
|
|
323
|
+
|
|
324
|
+
After every `goto`, `click` that triggers navigation, or `fill` + `submit` sequence:
|
|
325
|
+
|
|
326
|
+
1. **Wait for page load** — wait for `networkidle` or 5-second timeout, whichever comes first
|
|
327
|
+
2. **Check URL** — if expected URL pattern was provided, verify current URL matches
|
|
328
|
+
3. **Check title** — if expected title pattern was provided, verify page title matches
|
|
329
|
+
4. **Check for error indicators** — scan for common error patterns:
|
|
330
|
+
- HTTP error codes in title (403, 404, 500, 502, 503)
|
|
331
|
+
- Error modals or alert dialogs
|
|
332
|
+
- "Access Denied", "Unauthorized", "Session Expired" text in page
|
|
333
|
+
5. **Check for auth redirects** — if current URL contains `/login`, `/signin`, `/sso`, `/oauth`, or `/saml` when it wasn't the intended destination, flag as auth redirect
|
|
334
|
+
|
|
335
|
+
### On Unexpected State
|
|
336
|
+
|
|
337
|
+
When verification detects a mismatch or error:
|
|
338
|
+
|
|
339
|
+
1. **Stop execution immediately** — do not proceed to the next browser step
|
|
340
|
+
2. **Take a screenshot** — capture the current page state
|
|
341
|
+
3. **Present the situation** to the user:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
⚠ Unexpected browser state during plan step [N]:
|
|
345
|
+
|
|
346
|
+
Expected: [expected URL/title/state]
|
|
347
|
+
Actual: [current URL/title]
|
|
348
|
+
Issue: [description — wrong page / error page / auth redirect / modal detected]
|
|
349
|
+
|
|
350
|
+
Screenshot: [path to screenshot]
|
|
351
|
+
|
|
352
|
+
Options:
|
|
353
|
+
1. retry — navigate again
|
|
354
|
+
2. skip — skip this browser step, continue plan
|
|
355
|
+
3. abort — stop plan execution entirely
|
|
356
|
+
4. manual — open interactive browser for manual intervention
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
4. **Wait for user choice** — do not proceed without explicit selection
|
|
360
|
+
|
|
361
|
+
### Auth Redirect Detection
|
|
362
|
+
|
|
363
|
+
Authentication redirects are especially dangerous because silently following them can:
|
|
364
|
+
- Leak credentials to unexpected domains
|
|
365
|
+
- Complete OAuth flows the user didn't intend
|
|
366
|
+
- Grant permissions the user didn't approve
|
|
367
|
+
|
|
368
|
+
When an auth redirect is detected:
|
|
369
|
+
- NEVER automatically follow it
|
|
370
|
+
- Flag it prominently: "Auth redirect detected — redirected to [domain]"
|
|
371
|
+
- The user must explicitly choose to proceed
|
|
372
|
+
|
|
373
|
+
### Audit Trail
|
|
374
|
+
|
|
375
|
+
Every browser step within a plan produces:
|
|
376
|
+
- **Before screenshot** — taken just before the action
|
|
377
|
+
- **After screenshot** — taken after the action completes (or after error)
|
|
378
|
+
- **Timing** — how long the action took
|
|
379
|
+
- **Verification result** — PASS or FAIL with details
|
|
380
|
+
|
|
381
|
+
Screenshots saved to a temp directory, paths included in the step report.
|
|
382
|
+
|
|
383
|
+
### Non-Plan Usage
|
|
384
|
+
|
|
385
|
+
When ftm-browse is used directly (not within a plan), supervised mode is OFF by default. The user gets the raw browse experience. They can enable it manually with `--supervised` flag.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Error Handling
|
|
390
|
+
|
|
391
|
+
| Symptom | Cause | Fix |
|
|
392
|
+
|---|---|---|
|
|
393
|
+
| First command hangs up to 10s | Daemon cold start | Normal — wait for it |
|
|
394
|
+
| `Ref @eN not found. The page may have changed` | Stale ref after navigation | Re-run `snapshot -i` |
|
|
395
|
+
| `Ref @eN no longer exists on the page` | Element removed from DOM | Re-run `snapshot -i` |
|
|
396
|
+
| `Timeout` on goto | Page slow to load or wrong URL | Check URL, verify server is running |
|
|
397
|
+
| `Browser not installed` or Chromium launch error | Playwright Chromium missing | Run `npx playwright install chromium` |
|
|
398
|
+
| `Daemon failed to start within 10 seconds` | Bun or binary issue | Check `~/.ftm-browse/` for logs; verify binary is executable |
|
|
399
|
+
| Connection refused | Daemon died (idle timeout or crash) | Next command will auto-restart it |
|
|
400
|
+
| `commands must be an array` | Bad JSON passed to chain | Validate JSON before passing to chain |
|
|
401
|
+
| `Evaluation failed: ...` | Playwright could not serialize or run the expression | Check for syntax errors; wrap complex expressions in quotes |
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Tips
|
|
406
|
+
|
|
407
|
+
- Always run `snapshot -i` before `click` or `fill` — never guess or hardcode a ref number.
|
|
408
|
+
- Use `chain` for multi-step flows to reduce round-trip overhead; each step result is available in the returned array.
|
|
409
|
+
- Screenshots are cheap — take them liberally at key points (before interaction, after submit, after navigation) as a natural audit trail.
|
|
410
|
+
- The daemon persists across all commands in a session. Cold start only happens once per 30-minute idle window.
|
|
411
|
+
- `$PB text` is the fastest way to assert page content without parsing HTML.
|
|
412
|
+
- `$PB html` is useful when you need to inspect the raw DOM, check for hidden elements, or verify server-rendered markup.
|
|
413
|
+
- The daemon uses a 1280x800 headless Chromium viewport with a standard Mac Chrome user-agent, so most sites render predictably.
|
|
414
|
+
- To stop the daemon explicitly: `$PB stop`. It will auto-restart on next use.
|
|
415
|
+
- `$PB eval` is the escape hatch for anything the ARIA tree doesn't expose — hidden inputs, JS globals, localStorage, computed values.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
|
|
4
|
+
|
|
5
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
6
|
+
const DEFAULT_USER_DATA_DIR = path.join(process.env.HOME || "~", ".ftm-browse", "user-data");
|
|
7
|
+
|
|
8
|
+
class BrowserManager {
|
|
9
|
+
private browser: Browser | null = null;
|
|
10
|
+
private context: BrowserContext | null = null;
|
|
11
|
+
private page: Page | null = null;
|
|
12
|
+
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
private shutdownCallback: (() => void) | null = null;
|
|
14
|
+
private initPromise: Promise<void> | null = null;
|
|
15
|
+
private userDataDir: string | null = null;
|
|
16
|
+
|
|
17
|
+
setUserDataDir(dir: string | null): void {
|
|
18
|
+
this.userDataDir = dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setShutdownCallback(cb: () => void): void {
|
|
22
|
+
this.shutdownCallback = cb;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resetIdleTimer(): void {
|
|
26
|
+
if (this.idleTimer) {
|
|
27
|
+
clearTimeout(this.idleTimer);
|
|
28
|
+
}
|
|
29
|
+
this.idleTimer = setTimeout(() => {
|
|
30
|
+
console.log("[browser-manager] 30min idle timeout reached, shutting down");
|
|
31
|
+
void this.shutdown();
|
|
32
|
+
}, IDLE_TIMEOUT_MS);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async initialize(): Promise<void> {
|
|
36
|
+
// Short-circuit if already running (handles both ephemeral and persistent context modes)
|
|
37
|
+
if (this.isRunning()) return;
|
|
38
|
+
|
|
39
|
+
// Deduplicate concurrent init calls
|
|
40
|
+
if (this.initPromise) {
|
|
41
|
+
return this.initPromise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.initPromise = this._doInit().finally(() => {
|
|
45
|
+
this.initPromise = null;
|
|
46
|
+
});
|
|
47
|
+
return this.initPromise;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async _doInit(): Promise<void> {
|
|
51
|
+
const launchArgs = [
|
|
52
|
+
"--no-sandbox",
|
|
53
|
+
"--disable-setuid-sandbox",
|
|
54
|
+
"--disable-dev-shm-usage",
|
|
55
|
+
"--disable-gpu",
|
|
56
|
+
"--disable-background-networking",
|
|
57
|
+
"--disable-default-apps",
|
|
58
|
+
"--disable-extensions",
|
|
59
|
+
"--disable-sync",
|
|
60
|
+
"--disable-translate",
|
|
61
|
+
"--metrics-recording-only",
|
|
62
|
+
"--mute-audio",
|
|
63
|
+
"--no-first-run",
|
|
64
|
+
"--safebrowsing-disable-auto-update",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const contextOptions = {
|
|
68
|
+
viewport: { width: 1280, height: 800 } as { width: number; height: number } | null,
|
|
69
|
+
userAgent:
|
|
70
|
+
"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",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (this.userDataDir) {
|
|
74
|
+
// Persistent context — cookies and session data survive daemon restarts
|
|
75
|
+
const resolvedDir = this.userDataDir === "default" ? DEFAULT_USER_DATA_DIR : this.userDataDir;
|
|
76
|
+
console.log(`[browser-manager] Launching Chromium with persistent context at: ${resolvedDir}`);
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
79
|
+
fs.mkdirSync(resolvedDir, { recursive: true, mode: 0o700 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// launchPersistentContext returns a BrowserContext directly (no separate Browser)
|
|
83
|
+
this.context = await chromium.launchPersistentContext(resolvedDir, {
|
|
84
|
+
headless: true,
|
|
85
|
+
args: launchArgs,
|
|
86
|
+
...contextOptions,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Reuse existing page if available, otherwise open a new one
|
|
90
|
+
const existingPages = this.context.pages();
|
|
91
|
+
this.page = existingPages.length > 0 ? existingPages[0] : await this.context.newPage();
|
|
92
|
+
} else {
|
|
93
|
+
// Ephemeral context — no persistence (default, safe)
|
|
94
|
+
console.log("[browser-manager] Launching Chromium (ephemeral)...");
|
|
95
|
+
|
|
96
|
+
this.browser = await chromium.launch({
|
|
97
|
+
headless: true,
|
|
98
|
+
args: launchArgs,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
102
|
+
this.page = await this.context.newPage();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Start idle timer
|
|
106
|
+
this.resetIdleTimer();
|
|
107
|
+
|
|
108
|
+
console.log("[browser-manager] Browser ready");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getPage(): Promise<Page> {
|
|
112
|
+
await this.initialize();
|
|
113
|
+
this.resetIdleTimer();
|
|
114
|
+
|
|
115
|
+
if (!this.page || this.page.isClosed()) {
|
|
116
|
+
console.log("[browser-manager] Page was closed, creating new page");
|
|
117
|
+
this.page = await this.context!.newPage();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this.page;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getOrCreatePage(tabIndex?: number): Promise<Page> {
|
|
124
|
+
await this.initialize();
|
|
125
|
+
this.resetIdleTimer();
|
|
126
|
+
|
|
127
|
+
const pages = this.context!.pages();
|
|
128
|
+
|
|
129
|
+
if (tabIndex !== undefined && tabIndex < pages.length) {
|
|
130
|
+
this.page = pages[tabIndex];
|
|
131
|
+
return this.page;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!this.page || this.page.isClosed()) {
|
|
135
|
+
this.page = await this.context!.newPage();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.page;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getAllPages(): Promise<Page[]> {
|
|
142
|
+
if (!this.context) return [];
|
|
143
|
+
return this.context.pages();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async setActivePage(index: number): Promise<Page | null> {
|
|
147
|
+
if (!this.context) return null;
|
|
148
|
+
const pages = this.context.pages();
|
|
149
|
+
if (index < 0 || index >= pages.length) return null;
|
|
150
|
+
this.page = pages[index];
|
|
151
|
+
this.resetIdleTimer();
|
|
152
|
+
return this.page;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async shutdown(): Promise<void> {
|
|
156
|
+
if (this.idleTimer) {
|
|
157
|
+
clearTimeout(this.idleTimer);
|
|
158
|
+
this.idleTimer = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
if (this.page && !this.page.isClosed()) {
|
|
163
|
+
await this.page.close();
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Ignore close errors
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
if (this.context) {
|
|
171
|
+
await this.context.close();
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Ignore close errors
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
if (this.browser) {
|
|
179
|
+
await this.browser.close();
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Ignore close errors
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.page = null;
|
|
186
|
+
this.context = null;
|
|
187
|
+
this.browser = null;
|
|
188
|
+
|
|
189
|
+
console.log("[browser-manager] Browser shut down");
|
|
190
|
+
|
|
191
|
+
if (this.shutdownCallback) {
|
|
192
|
+
this.shutdownCallback();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
isRunning(): boolean {
|
|
197
|
+
// Persistent context mode: browser is null, check context instead
|
|
198
|
+
if (this.userDataDir) {
|
|
199
|
+
return this.context !== null;
|
|
200
|
+
}
|
|
201
|
+
return this.browser !== null && this.browser.isConnected();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Singleton instance
|
|
206
|
+
export const browserManager = new BrowserManager();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "ftm-browse",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"playwright": "^1.49.0",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
"packages": {
|
|
16
|
+
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
17
|
+
|
|
18
|
+
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
|
19
|
+
|
|
20
|
+
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
21
|
+
|
|
22
|
+
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
|
23
|
+
|
|
24
|
+
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
|
25
|
+
|
|
26
|
+
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
|
27
|
+
|
|
28
|
+
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
29
|
+
}
|
|
30
|
+
}
|