@hecer/yoke 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.claude-plugin/plugin.json +13 -13
  2. package/.codex-plugin/plugin.json +7 -7
  3. package/CHANGELOG.md +294 -288
  4. package/README.md +874 -874
  5. package/TODOS.md +5 -5
  6. package/agents/docs.toml +6 -6
  7. package/agents/implementer.toml +6 -6
  8. package/agents/reviewer.toml +6 -6
  9. package/agents/security.toml +6 -6
  10. package/bench/README.md +86 -86
  11. package/bench/RESULTS.md +35 -35
  12. package/bench/output-compaction.mjs +65 -65
  13. package/bench/result-schema.mjs +12 -12
  14. package/bench/results/claude-2026-07-27T18-03-26.json +50 -50
  15. package/bench/results/codex-unavailable-1785175418318.json +15 -15
  16. package/bench/results/gemini-2026-07-27T18-03-44.json +46 -46
  17. package/bench/run-matrix.mjs +26 -26
  18. package/bench/run.mjs +106 -106
  19. package/canon/AGENTS.md +30 -30
  20. package/canon/context/DECISIONS.md +4 -4
  21. package/canon/context/GLOSSARY.md +11 -11
  22. package/canon/context/KNOWLEDGE.md +4 -4
  23. package/canon/context/PROJECT.md +15 -15
  24. package/canon/loop/loop-spec.md +65 -65
  25. package/canon/loop/prd.schema.md +43 -43
  26. package/canon/manifest.yaml +59 -59
  27. package/canon/policy/gates.md +7 -7
  28. package/canon/policy/roles.md +9 -9
  29. package/canon/skills/ATTRIBUTION.md +99 -99
  30. package/canon/skills/authoring-prd/SKILL.md +58 -58
  31. package/canon/skills/brainstorming/SKILL.md +164 -164
  32. package/canon/skills/codebase-design/DEEPENING.md +15 -15
  33. package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -12
  34. package/canon/skills/codebase-design/SKILL.md +39 -39
  35. package/canon/skills/dispatching-parallel-agents/SKILL.md +182 -182
  36. package/canon/skills/document-release/SKILL.md +302 -302
  37. package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -19
  38. package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -39
  39. package/canon/skills/domain-modeling/SKILL.md +35 -35
  40. package/canon/skills/executing-plans/SKILL.md +70 -70
  41. package/canon/skills/finishing-a-development-branch/SKILL.md +200 -200
  42. package/canon/skills/health/SKILL.md +177 -177
  43. package/canon/skills/maintaining-context/SKILL.md +34 -34
  44. package/canon/skills/minimal-code/SKILL.md +21 -21
  45. package/canon/skills/no-ai-slop/SKILL.md +103 -103
  46. package/canon/skills/no-ai-slop/eval.md +43 -43
  47. package/canon/skills/plan-ceo-review/SKILL.md +541 -541
  48. package/canon/skills/plan-eng-review/SKILL.md +362 -362
  49. package/canon/skills/receiving-code-review/SKILL.md +213 -213
  50. package/canon/skills/requesting-code-review/SKILL.md +105 -105
  51. package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -18
  52. package/canon/skills/retro/SKILL.md +397 -397
  53. package/canon/skills/review/SKILL.md +246 -246
  54. package/canon/skills/ship/SKILL.md +691 -691
  55. package/canon/skills/subagent-driven-development/SKILL.md +277 -277
  56. package/canon/skills/systematic-debugging/SKILL.md +296 -296
  57. package/canon/skills/tdd/SKILL.md +371 -371
  58. package/canon/skills/unslop-ui/SKILL.md +34 -34
  59. package/canon/skills/using-git-worktrees/SKILL.md +218 -218
  60. package/canon/skills/verification-before-completion/SKILL.md +139 -139
  61. package/canon/skills/visual-verification/SKILL.md +54 -54
  62. package/canon/skills/workflow/SKILL.md +22 -22
  63. package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -27
  64. package/canon/skills/writing-for-agents/SKILL.md +42 -42
  65. package/canon/skills/writing-plans/SKILL.md +152 -152
  66. package/canon/skills/writing-skills/SKILL.md +655 -655
  67. package/canon/skills/yoke-retrofit/SKILL.md +26 -26
  68. package/canon/skills/yoke-workflow/SKILL.md +20 -20
  69. package/canon/tools/codex-rtk-hook.mjs +35 -35
  70. package/canon/tools/graphify.md +3 -3
  71. package/canon/tools/playwright-mcp.md +3 -3
  72. package/canon/tools/rtk.md +7 -7
  73. package/canon/tools/serena.md +6 -6
  74. package/dist/agents/process.js +3 -0
  75. package/dist/loop/watchdog.js +1 -1
  76. package/dist/prd/command.js +17 -17
  77. package/dist/retrofit/planners/claude.js +14 -14
  78. package/dist/retrofit/preserve.js +2 -2
  79. package/docs/MIGRATING-TO-1.0.md +33 -33
  80. package/docs/MIGRATING-TO-1.1.md +27 -27
  81. package/docs/MIGRATING-TO-1.4.md +70 -70
  82. package/docs/PUBLISHING.md +91 -91
  83. package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -981
  84. package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -258
  85. package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -1006
  86. package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -374
  87. package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -450
  88. package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -1024
  89. package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -574
  90. package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -537
  91. package/docs/superpowers/plans/2026-08-16-artifact-backed-output-compaction.md +329 -329
  92. package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -146
  93. package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
  94. package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
  95. package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
  96. package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
  97. package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
  98. package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
  99. package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
  100. package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
  101. package/gemini-extension.json +6 -6
  102. package/hooks/hooks.json +19 -19
  103. package/package.json +87 -87
@@ -1,155 +1,155 @@
1
- # Baustein M — `yoke flow-smoke`: built-in browser gate with screenshot/video proofs
2
-
3
- Date: 2026-07-02
4
- Status: approved (design delegated by user; proof concept approved in conversation: screenshots
5
- always, video on failure, `.yoke/proof/` storage, loop links proofs to stories)
6
-
7
- ## Problem
8
-
9
- The verify gate is code-only unless an agent hand-rolls a Playwright smoke (the
10
- `visual-verification` skill teaches it, but nothing enforces or standardizes it). A story can
11
- pass unit tests and design-scan yet ship a blank page, an unwired route, or a runtime console
12
- error. And when a story IS done, there is no visual evidence — "done with a photo" is the answer
13
- to the loudest agentic-coding pain point ("agent says done, but it isn't").
14
-
15
- ## Goal
16
-
17
- A built-in, mechanical browser gate that any project can add to `verify.command`:
18
-
19
- ```
20
- yoke flow-smoke [dir] [--url=<baseUrl>] [--label=<name>]
21
- ```
22
-
23
- For every configured flow: load the route, optionally wait for a landmark selector, assert zero
24
- console/page errors, and **always** save a screenshot to `.yoke/proof/<label>/<flow>.png`.
25
- Record video per flow but **keep it only on failure** (`<flow>.webm`). Exit 0 = all flows green
26
- (chainable in `verify.command`), 1 = failures, 2 = not runnable (no config / no playwright).
27
-
28
- ## Part 1: Config schema
29
-
30
- Extend `YokeConfigSchema` (src/retrofit/config.ts) with an optional `smoke` section:
31
-
32
- ```yaml
33
- smoke:
34
- baseUrl: http://localhost:3000
35
- flows:
36
- - name: home
37
- path: /
38
- landmark: "main h1" # optional CSS selector
39
- - name: login
40
- path: /login
41
- ```
42
-
43
- Zod: `smoke: z.object({ baseUrl: z.string().min(1), flows: z.array(z.object({ name: z.string().min(1), path: z.string().min(1), landmark: z.string().optional() })).min(1) }).optional()` and the matching optional field on the `YokeConfig` interface. Existing configs (no `smoke`) stay valid.
44
-
45
- ## Part 2: `runFlowSmoke` (src/smoke/command.ts)
46
-
47
- `export async function runFlowSmoke(targetDir: string, opts: FlowSmokeOptions = {}): Promise<number>`
48
-
49
- Sequence:
50
- 1. Load config. No config file or no `smoke` section → print guidance (example YAML above) and
51
- exit **2**. `--url` overrides `baseUrl`.
52
- 2. Resolve Playwright **from the target project** (never a Yoke dependency):
53
- `createRequire(join(targetDir, 'package.json')).resolve('playwright')`, then dynamic
54
- `import(pathToFileURL(resolved).href)`. Resolution failure → print
55
- `Playwright not found in <dir>. Install it: npm i -D playwright && npx playwright install chromium`
56
- and exit **2**.
57
- 3. Proof dir: `.yoke/proof/<label>/` where label = `opts.label` ?? `process.env.YOKE_STORY` ??
58
- `'latest'`. Wipe the label dir before the run (`rmSync(recursive, force)` + mkdir) — each run
59
- is fresh evidence, no stale screenshots.
60
- 4. Launch chromium headless once; per flow, create a **new context** with
61
- `recordVideo: { dir: <proofDir>/.video-tmp }` and a page. Collect errors: `page.on('console')`
62
- with `type() === 'error'`, and `page.on('pageerror')`.
63
- 5. Per flow, in order (fail-fast per flow, continue to the next flow):
64
- - `page.goto(baseUrl + path, { waitUntil: 'load', timeout: 30_000 })`; a non-OK response
65
- (`!response.ok()`) is a failure (`HTTP <status>`).
66
- - if `landmark`: `page.waitForSelector(landmark, { timeout: 10_000 })`; timeout → failure
67
- (`landmark "<sel>" not found`).
68
- - collected errors non-empty → failure (`N console error(s): <first, truncated 200 chars>`).
69
- - **always** `page.screenshot({ path: <proofDir>/<flow.name>.png, fullPage: true })` — also on
70
- failure (the failure screenshot IS the evidence), inside try/catch (a crashed page must not
71
- mask the original failure).
72
- - close context; then: flow failed → move its video to `<proofDir>/<flow.name>.webm`
73
- (`page.video().path()` after close); flow passed → delete the video file. Remove
74
- `.video-tmp` at the end.
75
- 6. Report per flow: `✔ home (screenshot: .yoke/proof/latest/home.png)` /
76
- `✘ login — 2 console error(s): ... (screenshot + video saved)`. Summary line
77
- `Flow-smoke: N/M flows green — proof: .yoke/proof/<label>/`.
78
- 7. Close the browser in a `finally`. Exit 0 all green, 1 any failure.
79
-
80
- Injectable seam for tests: `opts.browser?: () => Promise<SmokeBrowser>` — a minimal structural
81
- interface defined in the module:
82
-
83
- ```ts
84
- export interface SmokePage {
85
- goto(url: string, opts?: object): Promise<{ ok(): boolean; status(): number } | null>
86
- waitForSelector(sel: string, opts?: object): Promise<unknown>
87
- screenshot(opts: { path: string; fullPage?: boolean }): Promise<unknown>
88
- on(event: 'console' | 'pageerror', handler: (arg: any) => void): void
89
- video(): { path(): Promise<string> } | null
90
- }
91
- export interface SmokeContext { newPage(): Promise<SmokePage>; close(): Promise<void> }
92
- export interface SmokeBrowser {
93
- newContext(opts?: object): Promise<SmokeContext>
94
- close(): Promise<void>
95
- }
96
- ```
97
-
98
- The real path adapts Playwright's chromium to this interface; tests inject fakes (no Playwright
99
- in Yoke's devDependencies — an fs-level fake writes marker files for "screenshot"/"video").
100
-
101
- ## Part 3: Loop linkage — proofs per story
102
-
103
- - In `src/loop/loop.ts`, both verify call sites set `process.env.YOKE_STORY = story.id` before
104
- `opts.verify(...)` and delete it in a `finally`. A `yoke flow-smoke` inside `verify.command`
105
- then writes to `.yoke/proof/<story-id>/` automatically — every completed story has visual
106
- evidence, every blocked story has failure evidence (screenshot + video).
107
- - `.yoke/proof/` joins `YOKE_IGNORE_LINES` (runtime artifact; must not break the clean-tree gate).
108
-
109
- ## Part 4: CLI + async main
110
-
111
- - `main()` in src/cli.ts becomes `number | Promise<number>`-returning; the isMain block awaits it
112
- (top-level await, ESM). Existing sync cases are untouched.
113
- - `case 'flow-smoke'`: `[dir]`, `--url=`, `--label=` → `return runFlowSmoke(targetDir, { url, label })`.
114
- - Usage line gains `flow-smoke [dir] [--url=<baseUrl>] [--label=<name>]`.
115
-
116
- ## Part 5: Canon skill update
117
-
118
- `canon/skills/visual-verification/SKILL.md`: replace the hand-rolled flow-smoke instruction with
119
- the built-in — configure `smoke:` in `.yoke/config.yaml`, chain
120
- `... && yoke design-scan . && yoke flow-smoke .` in `verify.command`; keep the Playwright-MCP
121
- guidance for *debugging* a failed flow (watch the saved video first). Mention proofs land in
122
- `.yoke/proof/<story>/`. Skill count stays 27 (content update, no new skill).
123
-
124
- ## Testing
125
-
126
- - `tests/retrofit/config.test.ts` (extend): smoke section round-trips; config without smoke stays
127
- valid; invalid smoke (empty flows) rejected.
128
- - `tests/smoke/command.test.ts` (fake browser):
129
- - exit 2 when no smoke config (message contains example);
130
- - exit 2 when playwright is unresolvable: call without `opts.browser` (the seam bypasses
131
- resolution) against a temp dir that has a smoke config but no playwright install;
132
- - green flow: screenshot file written under `.yoke/proof/latest/<name>.png`, video deleted, exit 0;
133
- - landmark timeout → exit 1, screenshot still written, video kept as `<name>.webm`;
134
- - console error collected → exit 1;
135
- - non-OK response → exit 1;
136
- - label resolution: `--label` beats `YOKE_STORY` env beats `latest`;
137
- - proof dir wiped between runs (stale file gone);
138
- - one failing flow does not stop later flows (both reported).
139
- - `tests/loop/*`: one test that a fake verifier observes `process.env.YOKE_STORY === story.id`
140
- during verify and that it is unset afterwards (both normal and `--isolate` paths if cheap;
141
- normal path suffices).
142
- - `tests/retrofit/gitignore.test.ts`: `.yoke/proof/` ensured.
143
-
144
- ## Non-goals
145
-
146
- - No dev-server startup/readiness management (`start-server-and-test` and friends exist; the
147
- skill documents chaining). A connection-refused goto is an ordinary flow failure.
148
- - No visual diffing/pixel comparison, no multi-browser matrix, no video-always mode (token/disk
149
- cost; Ebene C in the Baustein-I backlog stays opt-in future work).
150
- - Playwright stays a target-project dependency, never Yoke's.
151
-
152
- ## Attribution
153
-
154
- Browser-QA-as-gate idea: gstack `/qa` (MIT © Garry Tan), natively re-implemented cross-agent with
155
- a proof-artifact contract; no code copied. Extend the existing ATTRIBUTION.md gstack entry.
1
+ # Baustein M — `yoke flow-smoke`: built-in browser gate with screenshot/video proofs
2
+
3
+ Date: 2026-07-02
4
+ Status: approved (design delegated by user; proof concept approved in conversation: screenshots
5
+ always, video on failure, `.yoke/proof/` storage, loop links proofs to stories)
6
+
7
+ ## Problem
8
+
9
+ The verify gate is code-only unless an agent hand-rolls a Playwright smoke (the
10
+ `visual-verification` skill teaches it, but nothing enforces or standardizes it). A story can
11
+ pass unit tests and design-scan yet ship a blank page, an unwired route, or a runtime console
12
+ error. And when a story IS done, there is no visual evidence — "done with a photo" is the answer
13
+ to the loudest agentic-coding pain point ("agent says done, but it isn't").
14
+
15
+ ## Goal
16
+
17
+ A built-in, mechanical browser gate that any project can add to `verify.command`:
18
+
19
+ ```
20
+ yoke flow-smoke [dir] [--url=<baseUrl>] [--label=<name>]
21
+ ```
22
+
23
+ For every configured flow: load the route, optionally wait for a landmark selector, assert zero
24
+ console/page errors, and **always** save a screenshot to `.yoke/proof/<label>/<flow>.png`.
25
+ Record video per flow but **keep it only on failure** (`<flow>.webm`). Exit 0 = all flows green
26
+ (chainable in `verify.command`), 1 = failures, 2 = not runnable (no config / no playwright).
27
+
28
+ ## Part 1: Config schema
29
+
30
+ Extend `YokeConfigSchema` (src/retrofit/config.ts) with an optional `smoke` section:
31
+
32
+ ```yaml
33
+ smoke:
34
+ baseUrl: http://localhost:3000
35
+ flows:
36
+ - name: home
37
+ path: /
38
+ landmark: "main h1" # optional CSS selector
39
+ - name: login
40
+ path: /login
41
+ ```
42
+
43
+ Zod: `smoke: z.object({ baseUrl: z.string().min(1), flows: z.array(z.object({ name: z.string().min(1), path: z.string().min(1), landmark: z.string().optional() })).min(1) }).optional()` and the matching optional field on the `YokeConfig` interface. Existing configs (no `smoke`) stay valid.
44
+
45
+ ## Part 2: `runFlowSmoke` (src/smoke/command.ts)
46
+
47
+ `export async function runFlowSmoke(targetDir: string, opts: FlowSmokeOptions = {}): Promise<number>`
48
+
49
+ Sequence:
50
+ 1. Load config. No config file or no `smoke` section → print guidance (example YAML above) and
51
+ exit **2**. `--url` overrides `baseUrl`.
52
+ 2. Resolve Playwright **from the target project** (never a Yoke dependency):
53
+ `createRequire(join(targetDir, 'package.json')).resolve('playwright')`, then dynamic
54
+ `import(pathToFileURL(resolved).href)`. Resolution failure → print
55
+ `Playwright not found in <dir>. Install it: npm i -D playwright && npx playwright install chromium`
56
+ and exit **2**.
57
+ 3. Proof dir: `.yoke/proof/<label>/` where label = `opts.label` ?? `process.env.YOKE_STORY` ??
58
+ `'latest'`. Wipe the label dir before the run (`rmSync(recursive, force)` + mkdir) — each run
59
+ is fresh evidence, no stale screenshots.
60
+ 4. Launch chromium headless once; per flow, create a **new context** with
61
+ `recordVideo: { dir: <proofDir>/.video-tmp }` and a page. Collect errors: `page.on('console')`
62
+ with `type() === 'error'`, and `page.on('pageerror')`.
63
+ 5. Per flow, in order (fail-fast per flow, continue to the next flow):
64
+ - `page.goto(baseUrl + path, { waitUntil: 'load', timeout: 30_000 })`; a non-OK response
65
+ (`!response.ok()`) is a failure (`HTTP <status>`).
66
+ - if `landmark`: `page.waitForSelector(landmark, { timeout: 10_000 })`; timeout → failure
67
+ (`landmark "<sel>" not found`).
68
+ - collected errors non-empty → failure (`N console error(s): <first, truncated 200 chars>`).
69
+ - **always** `page.screenshot({ path: <proofDir>/<flow.name>.png, fullPage: true })` — also on
70
+ failure (the failure screenshot IS the evidence), inside try/catch (a crashed page must not
71
+ mask the original failure).
72
+ - close context; then: flow failed → move its video to `<proofDir>/<flow.name>.webm`
73
+ (`page.video().path()` after close); flow passed → delete the video file. Remove
74
+ `.video-tmp` at the end.
75
+ 6. Report per flow: `✔ home (screenshot: .yoke/proof/latest/home.png)` /
76
+ `✘ login — 2 console error(s): ... (screenshot + video saved)`. Summary line
77
+ `Flow-smoke: N/M flows green — proof: .yoke/proof/<label>/`.
78
+ 7. Close the browser in a `finally`. Exit 0 all green, 1 any failure.
79
+
80
+ Injectable seam for tests: `opts.browser?: () => Promise<SmokeBrowser>` — a minimal structural
81
+ interface defined in the module:
82
+
83
+ ```ts
84
+ export interface SmokePage {
85
+ goto(url: string, opts?: object): Promise<{ ok(): boolean; status(): number } | null>
86
+ waitForSelector(sel: string, opts?: object): Promise<unknown>
87
+ screenshot(opts: { path: string; fullPage?: boolean }): Promise<unknown>
88
+ on(event: 'console' | 'pageerror', handler: (arg: any) => void): void
89
+ video(): { path(): Promise<string> } | null
90
+ }
91
+ export interface SmokeContext { newPage(): Promise<SmokePage>; close(): Promise<void> }
92
+ export interface SmokeBrowser {
93
+ newContext(opts?: object): Promise<SmokeContext>
94
+ close(): Promise<void>
95
+ }
96
+ ```
97
+
98
+ The real path adapts Playwright's chromium to this interface; tests inject fakes (no Playwright
99
+ in Yoke's devDependencies — an fs-level fake writes marker files for "screenshot"/"video").
100
+
101
+ ## Part 3: Loop linkage — proofs per story
102
+
103
+ - In `src/loop/loop.ts`, both verify call sites set `process.env.YOKE_STORY = story.id` before
104
+ `opts.verify(...)` and delete it in a `finally`. A `yoke flow-smoke` inside `verify.command`
105
+ then writes to `.yoke/proof/<story-id>/` automatically — every completed story has visual
106
+ evidence, every blocked story has failure evidence (screenshot + video).
107
+ - `.yoke/proof/` joins `YOKE_IGNORE_LINES` (runtime artifact; must not break the clean-tree gate).
108
+
109
+ ## Part 4: CLI + async main
110
+
111
+ - `main()` in src/cli.ts becomes `number | Promise<number>`-returning; the isMain block awaits it
112
+ (top-level await, ESM). Existing sync cases are untouched.
113
+ - `case 'flow-smoke'`: `[dir]`, `--url=`, `--label=` → `return runFlowSmoke(targetDir, { url, label })`.
114
+ - Usage line gains `flow-smoke [dir] [--url=<baseUrl>] [--label=<name>]`.
115
+
116
+ ## Part 5: Canon skill update
117
+
118
+ `canon/skills/visual-verification/SKILL.md`: replace the hand-rolled flow-smoke instruction with
119
+ the built-in — configure `smoke:` in `.yoke/config.yaml`, chain
120
+ `... && yoke design-scan . && yoke flow-smoke .` in `verify.command`; keep the Playwright-MCP
121
+ guidance for *debugging* a failed flow (watch the saved video first). Mention proofs land in
122
+ `.yoke/proof/<story>/`. Skill count stays 27 (content update, no new skill).
123
+
124
+ ## Testing
125
+
126
+ - `tests/retrofit/config.test.ts` (extend): smoke section round-trips; config without smoke stays
127
+ valid; invalid smoke (empty flows) rejected.
128
+ - `tests/smoke/command.test.ts` (fake browser):
129
+ - exit 2 when no smoke config (message contains example);
130
+ - exit 2 when playwright is unresolvable: call without `opts.browser` (the seam bypasses
131
+ resolution) against a temp dir that has a smoke config but no playwright install;
132
+ - green flow: screenshot file written under `.yoke/proof/latest/<name>.png`, video deleted, exit 0;
133
+ - landmark timeout → exit 1, screenshot still written, video kept as `<name>.webm`;
134
+ - console error collected → exit 1;
135
+ - non-OK response → exit 1;
136
+ - label resolution: `--label` beats `YOKE_STORY` env beats `latest`;
137
+ - proof dir wiped between runs (stale file gone);
138
+ - one failing flow does not stop later flows (both reported).
139
+ - `tests/loop/*`: one test that a fake verifier observes `process.env.YOKE_STORY === story.id`
140
+ during verify and that it is unset afterwards (both normal and `--isolate` paths if cheap;
141
+ normal path suffices).
142
+ - `tests/retrofit/gitignore.test.ts`: `.yoke/proof/` ensured.
143
+
144
+ ## Non-goals
145
+
146
+ - No dev-server startup/readiness management (`start-server-and-test` and friends exist; the
147
+ skill documents chaining). A connection-refused goto is an ordinary flow failure.
148
+ - No visual diffing/pixel comparison, no multi-browser matrix, no video-always mode (token/disk
149
+ cost; Ebene C in the Baustein-I backlog stays opt-in future work).
150
+ - Playwright stays a target-project dependency, never Yoke's.
151
+
152
+ ## Attribution
153
+
154
+ Browser-QA-as-gate idea: gstack `/qa` (MIT © Garry Tan), natively re-implemented cross-agent with
155
+ a proof-artifact contract; no code copied. Extend the existing ATTRIBUTION.md gstack entry.