@rubytech/create-maxy-code 0.1.108 → 0.1.110

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 (124) hide show
  1. package/dist/index.js +11 -125
  2. package/dist/snap-chromium.js +1 -2
  3. package/dist/uninstall.js +10 -9
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/admin/skills/upgrade/SKILL.md +34 -0
  9. package/payload/platform/plugins/cloudflare/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/cloudflare/PLUGIN.md +9 -16
  11. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +7 -12
  12. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/cloudflare/references/dashboard-guide.md +3 -3
  14. package/payload/platform/plugins/cloudflare/references/manual-setup.md +16 -51
  15. package/payload/platform/plugins/cloudflare/references/reset-guide.md +24 -25
  16. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +29 -144
  17. package/payload/platform/plugins/docs/references/admin-session.md +1 -1
  18. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  19. package/payload/platform/plugins/docs/references/cloudflare.md +20 -29
  20. package/payload/platform/plugins/docs/references/platform.md +4 -22
  21. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  22. package/payload/platform/plugins/docs/references/troubleshooting.md +4 -316
  23. package/payload/platform/plugins/venture-studio/PLUGIN.md +35 -4
  24. package/payload/platform/plugins/venture-studio/bin/scaffold.sh +104 -0
  25. package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +3 -1
  26. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts +2 -0
  27. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts.map +1 -0
  28. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js +78 -0
  29. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js.map +1 -0
  30. package/payload/platform/scripts/check-no-task-id-leaks.mjs +1 -1
  31. package/payload/platform/scripts/vnc.sh +4 -3
  32. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  33. package/payload/premium-plugins/venture-studio/PLUGIN.md +35 -4
  34. package/payload/premium-plugins/venture-studio/bin/scaffold.sh +104 -0
  35. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +3 -1
  36. package/payload/server/{chunk-AGFS3TVN.js → chunk-FSPPVVWM.js} +1303 -496
  37. package/payload/server/maxy-edge.js +21 -259
  38. package/payload/server/public/assets/{ChatInput-DJsqm_Gf.js → ChatInput-CsnIedhS.js} +1 -5
  39. package/payload/server/public/assets/{Checkbox-DGZG9BKc.js → Checkbox-CWugFyFT.js} +1 -1
  40. package/payload/server/public/assets/admin-D3gZuyUn.js +217 -0
  41. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Dw16BhiX.js → architectureDiagram-Q4EWVU46-CeTRKWDb.js} +1 -1
  42. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DIOpmf5Y.js → blockDiagram-DXYQGD6D-DeeIX5U3.js} +1 -1
  43. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Tdb_HZeX.js → c4Diagram-AHTNJAMY-CFsqZuil.js} +1 -1
  44. package/payload/server/public/assets/channel-CrSx5mnG.js +1 -0
  45. package/payload/server/public/assets/{chunk-336JU56O-CebpwDDe.js → chunk-336JU56O-DpIXuFM0.js} +2 -2
  46. package/payload/server/public/assets/{chunk-426QAEUC-BtRCmfDU.js → chunk-426QAEUC-Qk8qqrvA.js} +1 -1
  47. package/payload/server/public/assets/{chunk-4TB4RGXK-BZ3GEWs3.js → chunk-4TB4RGXK-DtM8-CUn.js} +1 -1
  48. package/payload/server/public/assets/{chunk-5FUZZQ4R-iDI6Xu0U.js → chunk-5FUZZQ4R-CrSQ4ySU.js} +1 -1
  49. package/payload/server/public/assets/{chunk-5PVQY5BW-SQD_EpYa.js → chunk-5PVQY5BW-Bn2nQwdj.js} +1 -1
  50. package/payload/server/public/assets/{chunk-EDXVE4YY-CtCcA7_e.js → chunk-EDXVE4YY-CzCPnR0P.js} +1 -1
  51. package/payload/server/public/assets/{chunk-ENJZ2VHE-pXVGVCbb.js → chunk-ENJZ2VHE-CgZj9RoG.js} +1 -1
  52. package/payload/server/public/assets/{chunk-ICPOFSXX-Dkzg9o2N.js → chunk-ICPOFSXX-wy-eNjwW.js} +1 -1
  53. package/payload/server/public/assets/{chunk-OYMX7WX6-1ZZWzf9F.js → chunk-OYMX7WX6-CMmJtL8S.js} +1 -1
  54. package/payload/server/public/assets/{chunk-U2HBQHQK-CpQ3kzO0.js → chunk-U2HBQHQK-CFCW7OaT.js} +1 -1
  55. package/payload/server/public/assets/{chunk-X2U36JSP-C2LkxroC.js → chunk-X2U36JSP-Bgh-CJSN.js} +1 -1
  56. package/payload/server/public/assets/{chunk-YZCP3GAM-Bl5jBOt5.js → chunk-YZCP3GAM-BXKwZ4vN.js} +1 -1
  57. package/payload/server/public/assets/{chunk-ZZ45TVLE-CHtnptPS.js → chunk-ZZ45TVLE-BiOuK5NP.js} +1 -1
  58. package/payload/server/public/assets/classDiagram-6PBFFD2Q-C3IDJsqN.js +1 -0
  59. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BOIrJ5Zb.js +1 -0
  60. package/payload/server/public/assets/clone-P8Fkz7JD.js +1 -0
  61. package/payload/server/public/assets/{dagre-BziN0Nkh.js → dagre-DyCxa9Q2.js} +1 -1
  62. package/payload/server/public/assets/{dagre-KV5264BT-B7OG1g1Y.js → dagre-KV5264BT-Cffo_hmf.js} +1 -1
  63. package/payload/server/public/assets/data-DRfwJPja.js +1 -0
  64. package/payload/server/public/assets/{diagram-5BDNPKRD-Bf31nIDs.js → diagram-5BDNPKRD-Bt93Du3V.js} +1 -1
  65. package/payload/server/public/assets/{diagram-G4DWMVQ6-DQu85hhH.js → diagram-G4DWMVQ6-v4r-tBsC.js} +1 -1
  66. package/payload/server/public/assets/{diagram-MMDJMWI5-5tstbs4y.js → diagram-MMDJMWI5-DKrVPOP-.js} +1 -1
  67. package/payload/server/public/assets/{diagram-TYMM5635--MOV1U4o.js → diagram-TYMM5635-B1PzWQb9.js} +1 -1
  68. package/payload/server/public/assets/{erDiagram-SMLLAGMA-BtAnOJmd.js → erDiagram-SMLLAGMA-ClQzsQAs.js} +1 -1
  69. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CybiCIih.js → flowDiagram-DWJPFMVM-h3IbpN_7.js} +1 -1
  70. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-B44jN7Cy.js → ganttDiagram-T4ZO3ILL-ClaxOJi8.js} +1 -1
  71. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CT7iYcwg.js → gitGraphDiagram-UUTBAWPF-pM-U4ClD.js} +1 -1
  72. package/payload/server/public/assets/graph-B4FFYwus.js +1 -0
  73. package/payload/server/public/assets/graph-labels-Cpk9Ktt0.js +1 -0
  74. package/payload/server/public/assets/{graphlib-DP4o0pYL.js → graphlib-CwimOv_M.js} +1 -1
  75. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CvxFpFHN.js → infoDiagram-42DDH7IO-DcO3Giwe.js} +1 -1
  76. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CUwWLPeR.js → ishikawaDiagram-UXIWVN3A-BcdSL5VS.js} +1 -1
  77. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-y-OnGgLi.js → journeyDiagram-VCZTEJTY-CYahHYzf.js} +1 -1
  78. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BhrSe8R4.js → kanban-definition-6JOO6SKY-Cl5KhoS4.js} +1 -1
  79. package/payload/server/public/assets/lib--yuBd0Xi.js +33 -0
  80. package/payload/server/public/assets/{line-C9uS7z4J.js → line-d_2oTxLp.js} +1 -1
  81. package/payload/server/public/assets/{mermaid-parser.core-Cg4ZdKp-.js → mermaid-parser.core-CMMDGv3x.js} +1 -1
  82. package/payload/server/public/assets/{mermaid.core-BHdKOsex.js → mermaid.core-DK9ENGbr.js} +3 -3
  83. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-NBYiXHo7.js → mindmap-definition-QFDTVHPH-mA3x3MFG.js} +1 -1
  84. package/payload/server/public/assets/page-CgDmg_fX.js +1 -0
  85. package/payload/server/public/assets/{page-D9YpwhIu.js → page-D9lluVl7.js} +2 -2
  86. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CbojC64C.js → pieDiagram-DEJITSTG-CW1nGFlY.js} +1 -1
  87. package/payload/server/public/assets/public-DdLMkNdS.js +7 -0
  88. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CnnoZXcL.js → quadrantDiagram-34T5L4WZ-DI5igJhR.js} +1 -1
  89. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DFRFRtyJ.js → requirementDiagram-MS252O5E-DyVeI4e_.js} +1 -1
  90. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DU5gcpzO.js → sankeyDiagram-XADWPNL6-CCTTBYKY.js} +1 -1
  91. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BN7HZ6Hq.js → sequenceDiagram-FGHM5R23-hVVf35ly.js} +1 -1
  92. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DG6cCjvg.js → stateDiagram-FHFEXIEX-CSLuQDUX.js} +1 -1
  93. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-C5JejhQU.js +1 -0
  94. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-qmV-8SEV.js → timeline-definition-GMOUNBTQ-sxC3W8Q4.js} +1 -1
  95. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BMXQ1s3n.js → vennDiagram-DHZGUBPP-Ro-ton6Z.js} +1 -1
  96. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CoAdP1Gw.js → wardleyDiagram-NUSXRM2D-T-dKD0Zx.js} +1 -1
  97. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CZlfGTEn.js → xychartDiagram-5P7HB3ND-CfProPts.js} +1 -1
  98. package/payload/server/public/data.html +4 -4
  99. package/payload/server/public/graph.html +5 -5
  100. package/payload/server/public/index.html +7 -7
  101. package/payload/server/public/public.html +4 -4
  102. package/payload/server/server.js +299 -1392
  103. package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +0 -241
  104. package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +0 -154
  105. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +0 -98
  106. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +0 -749
  107. package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +0 -107
  108. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +0 -854
  109. package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +0 -291
  110. package/payload/server/chunk-BDFOTLPW.js +0 -759
  111. package/payload/server/chunk-JRBCOVA4.js +0 -1305
  112. package/payload/server/cloudflare-task-tracker-M5ONAGUT.js +0 -22
  113. package/payload/server/public/assets/admin-BvzMvMGo.js +0 -217
  114. package/payload/server/public/assets/channel-DThrH4QF.js +0 -1
  115. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BC6oGTNX.js +0 -1
  116. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-COwC5Umh.js +0 -1
  117. package/payload/server/public/assets/clone-Chp7hvnA.js +0 -1
  118. package/payload/server/public/assets/data-BAgaPs4j.js +0 -1
  119. package/payload/server/public/assets/graph-Cma7EArf.js +0 -1
  120. package/payload/server/public/assets/graph-labels-DyKk6Sxf.js +0 -1
  121. package/payload/server/public/assets/lib-CpkYtEDz.js +0 -29
  122. package/payload/server/public/assets/page-B4IWl3aZ.js +0 -1
  123. package/payload/server/public/assets/public-BtOXjy3A.js +0 -8
  124. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvVd1Q_q.js +0 -1
@@ -216,7 +216,7 @@ Two endpoints, two surfaces, two restart-survival roles:
216
216
  liveness. `processStartedAt` resets on every brand-service restart;
217
217
  Neo4j probe is bounded to 1 s and reports
218
218
  `conversationDb: 'ok' | 'error'`. Use this to confirm the brand
219
- process came back after `setup-tunnel.sh`'s armed restart.
219
+ process came back after a Cloudflare-setup armed restart.
220
220
  - `GET /api/admin/version` (maxy-edge) — installer / brand version
221
221
  string. Hosted on `maxy-edge.service` so the Software Update modal
222
222
  can read it while the brand service is mid-restart.
@@ -1,6 +1,6 @@
1
1
  # Cloudflare Tunnel — the dashboard is the source of truth
2
2
 
3
- Each installation has its own Cloudflare account. Sign-in is OAuth: the setup script prints the Cloudflare Authorize URL into the admin chat as a clickable link; click it, authorize in the new tab, and `cloudflared` writes `cert.pem` to the brand's config directory. The agent never reads or mutates Cloudflare account state directly — whatever you see in your logged-in dashboard is the single source of truth. When something needs doing on the account side (adding a domain, deleting a stray entry, switching accounts), the agent relays the click-paths; you run them in your browser.
3
+ Each installation has its own Cloudflare account. Sign-in is OAuth: the agent invokes `cloudflared tunnel login` via Bash; the Cloudflare Authorize URL streams into the admin chat PTY and the native terminal renders it as a clickable link. Click it, authorise in your own browser, and `cloudflared` writes `cert.pem` to the brand's config directory. The agent never reads or mutates Cloudflare account state directly — whatever you see in your logged-in dashboard is the single source of truth. When something needs doing on the account side (adding a domain, deleting a stray entry, switching accounts), the agent relays the click-paths; you run them in your browser.
4
4
 
5
5
  ## Identity model
6
6
 
@@ -8,34 +8,25 @@ Each installation has its own Cloudflare account. Sign-in is OAuth: the setup sc
8
8
  |------|--------|
9
9
  | **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
10
10
  | **Cloudflare account identity** | `cert.pem` from OAuth. One account per brand per device. |
11
- | **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard at script-invocation time via `list-cf-domains.sh`, not `brand.json`. Brand identity has no authority over which domains the operator's CF account holds. When the scrape returns an unexpected count (e.g. 1 on a two-zone account), the stream log's per-poll `phase=dom-scrape-poll n=<k> count=<n> domains=[…]` trajectory + the on-disk HTML dump at `~/{configDir}/logs/list-cf-domains-<ts>-count<n>-<mode>-pid<pid>.html` (earlier platform fixes — written on every scrape outcome, not just empty ones) give the operator everything they need to triage the cause without re-running. |
12
- | **CDP port the scrape attaches to** | `brand.json` (`cdpPort`, stamped at install time) — the same brand-scoped port `vnc.sh` binds Chromium to. `list-cf-domains.sh <brand>` requires the brand arg; the helper reads `${MAXY_PLATFORM_ROOT}/config/brand.json` (no silent default). Missing brand arg, missing brand.json, or missing `cdpPort` field each exit 1 with one of three named reasons (`brand-arg-missing`, `brand-config-missing`, `cdp-port-unresolved`); the agent reads the exit-code reason from stderr, names the brand, and stops — retrying would re-fire the same wrongly-resolved spawn. |
13
- | **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `tunnel.state`, `alias-domains.json`. |
11
+ | **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard the operator picks the zone in the dashboard during OAuth or names it in chat. The agent does not enumerate zones programmatically. |
12
+ | **Local tunnel state** | `~/{configDir}/cloudflared/` `cert.pem`, `<UUID>.json`, `config.yml`, `alias-domains.json`. |
14
13
 
15
- There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, run `reset-tunnel.sh` (which deletes the cert and every tunnel on the current account), then run `setup-tunnel.sh` again — `cloudflared tunnel login` inside the setup script will pick a fresh account when you sign in.
14
+ There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, the agent runs the reset flow from `plugins/cloudflare/references/reset-guide.md` (deletes the cert and every tunnel on the current account), then the manual-setup flow again — `cloudflared tunnel login` picks a fresh account when you sign in.
16
15
 
17
16
  ## Setup flow
18
17
 
19
- Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and then collects the inputs in plain chat:
18
+ Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and collects the inputs in plain chat:
20
19
 
21
20
  - **Admin address** — the hostname that will serve the admin chat (e.g. `admin.yourdomain.com`).
22
21
  - **Public address** — optional hostname for the public agent (e.g. `public.yourdomain.com` or `chat.yourdomain.com`).
23
22
  - **Proxy apex** — optional bare-domain hostname (e.g. `yourdomain.com`) that should also serve the public agent.
24
23
  - **Admin password** — the password used to gate remote access to the admin surface.
25
24
 
26
- Once the agent has the four inputs, it sets the admin password via `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password` (the same endpoint onboarding uses), optionally enumerates existing tunnels via `cloudflared tunnel list --output json` so you can pick from the logged-in account, and then invokes `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]` via the Bash tool with either `TUNNEL_ID=<uuid>` (selected) or `TUNNEL_NAME=<name>` (new) in env. After the script returns, the agent appends each non-`public.*` public or apex hostname to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public.
25
+ The agent then sets the admin password via `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password` (same endpoint the local onboarding form uses), and works through `plugins/cloudflare/references/manual-setup.md` Steps 1–7 directly via the Bash tool. `cloudflared`'s stdout streams into the PTY verbatim. The OAuth URL is linkified by the terminal; click it in your own browser to authorise. After the tunnel is up, the agent appends each non-`public.*` public or apex hostname to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public, and starts the brand's cloudflared user service.
27
26
 
28
- The script runs end-to-end:
27
+ If any step's `cloudflared` invocation exits non-zero, the agent names the literal exit code, surfaces the stderr verbatim, and cites `reset-guide.md` for the next action — no retry under a different flag, no Playwright-driven dashboard inspection.
29
28
 
30
- - `cloudflared tunnel login` OAuth sign-in. The script prints the Cloudflare authorize URL; the admin UI surfaces it as a clickable link above the log. Click the link, pick the account that owns your domain, click Authorize. `cert.pem` lands; the script picks it up via its 180 s cert-poll regardless of which browser tab completed the click.
31
- - Tunnel resolution from operator-supplied identity (operator-selected-tunnel fix). The script requires EXACTLY ONE of `TUNNEL_ID` or `TUNNEL_NAME` in env and exits 1 with a literal error on either-both-set or neither-set. Pre-fix the script derived `${BRAND}-$(hostname -s)` locally; that broke the operator-state-is-authoritative doctrine and silently created orphan tunnels whenever the device hostname changed. Stream log emits `step=tunnel-resolve source=operator-selected|operator-created tunnel_id=… tunnel_name=…` once the UUID is known.
32
- - **Zone pre-flight** — for every non-apex hostname the script queries `1.1.1.1` for the registrable parent's NS records and refuses the whole run if they don't point at Cloudflare. Stream log: `step=zone-preflight result=ok|error zones_on_account=… missing_parent_for=…`. Catches "domain not on Cloudflare"; does not catch "domain on a different Cloudflare account than `cert.pem` is bound to" — that case surfaces later via `tunnel-status`.
33
- - `cloudflared tunnel route dns` for each subdomain hostname. Apex hostnames cannot be routed this way — the script prints an **ACTION REQUIRED** block naming the exact dashboard record to add or edit. Stream log emits `step=route-dns hostname=… tunnel_id=…` before the call and `step=route-dns hostname=… result=ok|apex-skip|error` after; on error the bounded cloudflared stderr (≤400 chars) rides in the same phase line. **The script does not parse cloudflared's stdout** — exit code is the sole decision signal, so all three legitimate cloudflared output shapes (new record, overwrite, idempotent "already configured") are treated as success.
34
- - `config.yml` and `tunnel.state` written under `${CFG_DIR}`.
35
- - `systemctl --user restart ${BRAND}.service` — restarts the platform service so the new tunnel spawns via the service's `ExecStartPre=resume-tunnel.sh`. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically.
36
- - Post-restart verification — `ps -ef | grep '[c]loudflared'` confirms the connector is alive, then `curl -I https://<hostname>` against each subdomain (up to 60 s per host) confirms a non-530 response.
37
-
38
- The agent streams the script's stdout into chat verbatim as it arrives, including any `ACTION REQUIRED` block. If the script exits non-zero, the agent names the literal exit code and cites `reset-guide.md` for the next action — no retry under a different flag, no Playwright-driven dashboard inspection.
29
+ The setup-done claim only fires after the agent runs `curl -I https://<admin-hostname>` from outside the local network and the response shows a `200` line. That HTTP response is the only success terminal.
39
30
 
40
31
  ## Getting a domain on Cloudflare
41
32
 
@@ -49,19 +40,19 @@ Existing website traffic continues to work during and after the switch. Only DNS
49
40
 
50
41
  ## Reset / account switch
51
42
 
52
- Ask the agent to reset Cloudflare. The agent invokes `reset-tunnel.sh` on the device:
43
+ Ask the agent to reset Cloudflare. The agent executes the reset flow from `plugins/cloudflare/references/reset-guide.md`:
53
44
 
54
45
  - Deletes every tunnel on the brand's current Cloudflare account (via the bound cert).
55
46
  - Wipes the brand's `${CFG_DIR}`.
56
- - Exits with the number of tunnels deleted.
47
+ - Stops the brand's cloudflared user service.
57
48
 
58
- The script does **not** stop token-mode connector processes, delete stray misrouted CNAMEs in the dashboard, or touch the platform service. If any of those apply (agent will flag them), the agent guides you through the manual cleanup — `pkill -f 'cloudflared.*tunnel run --token'` on the device, or deleting the stray CNAME in the dashboard.
49
+ The agent does **not** stop token-mode connector processes or delete stray misrouted CNAMEs in the dashboard. If any of those apply, the agent guides you through the manual cleanup — `pkill -f 'cloudflared.*tunnel run --token'` on the device, or deleting the stray CNAME in the dashboard.
59
50
 
60
51
  After reset, run setup again. The fresh `cloudflared tunnel login` will pick whichever Cloudflare account you sign into.
61
52
 
62
53
  ## Manual runbook
63
54
 
64
- When the scripted flow fails partway, or you want to walk through every step yourself, there's a step-by-step runbook at `plugins/cloudflare/references/manual-setup.md`. The runbook mirrors `setup-tunnel.sh` verbatim every scripted step corresponds to a numbered runbook step with isolated command blocks, success conditions, and troubleshooting. Pick up the runbook at whichever step the script failed in.
55
+ The step-by-step runbook at `plugins/cloudflare/references/manual-setup.md` is the contract the agent follows. It is also what an operator runs by hand when needed every numbered step is an isolated `cloudflared` command block with success conditions and troubleshooting.
65
56
 
66
57
  ## Dashboard operations the CLI cannot do
67
58
 
@@ -71,17 +62,17 @@ The CLI cannot add a domain, switch accounts, edit an apex CNAME, or delete stra
71
62
 
72
63
  ### Tunnel won't start
73
64
 
74
- Ask the agent to check. The agent reads `systemctl --user status ${BRAND}.service` and `~/{configDir}/cloudflared/logs/cloudflared.log`. Common states:
65
+ Ask the agent to check. The agent reads `systemctl --user status ${BRAND}-cloudflared.service` and the cloudflared log under `~/{configDir}/cloudflared/`. Common states:
75
66
 
76
- - **No cloudflared process running** — the service's `ExecStartPre=resume-tunnel.sh` exited cleanly but did not spawn the connector. Usually means `tunnel.state` is missing or `config.yml` was written with empty variables. The agent will re-run `setup-tunnel.sh`.
77
- - **`tunnel not found`** — the UUID in `config.yml` does not match any tunnel on the currently-bound account. Usually follows an account switch that didn't reset local state. The agent invokes `reset-tunnel.sh` and then a fresh setup.
67
+ - **No cloudflared process running** — the cloudflared service exited or never started. The agent runs the manual-setup flow to re-issue tunnel creation.
68
+ - **`tunnel not found`** — the UUID in `config.yml` does not match any tunnel on the currently-bound account. Usually follows an account switch that didn't reset local state. The agent runs the reset flow and then a fresh setup.
78
69
 
79
70
  ### URL returns 530
80
71
 
81
72
  DNS propagation or account mismatch. Wait 30–60 seconds and retry first. If the 530 persists:
82
73
 
83
- - The domain may be on a Cloudflare account different from the one `cert.pem` is bound to — re-running `setup-tunnel.sh` re-validates.
84
- - The UDP buffer for QUIC may be undersized on this device — check `cloudflared.log` for `failed to sufficiently increase receive buffer size`.
74
+ - The domain may be on a Cloudflare account different from the one `cert.pem` is bound to — the agent re-runs the manual setup steps to re-validate.
75
+ - The UDP buffer for QUIC may be undersized on this device — check the cloudflared log for `failed to sufficiently increase receive buffer size`.
85
76
 
86
77
  ### URL returns connection refused
87
78
 
@@ -104,8 +95,8 @@ The most common cause is wrong nameservers on the domain. The domain must use Cl
104
95
 
105
96
  ## What the agent does and does not do
106
97
 
107
- **Does:** invokes `setup-tunnel.sh` / `reset-tunnel.sh` via Bash, quotes click-paths from the reference files verbatim, verifies external reachability with `curl -I`.
98
+ **Does:** invokes `cloudflared` directly via Bash, following `plugins/cloudflare/references/manual-setup.md` step by step; quotes click-paths from the reference files verbatim; verifies external reachability with `curl -I` and surfaces the response.
108
99
 
109
- **Does not:** drive the Cloudflare dashboard via Playwright, synthesise alternative `cloudflared` flag sequences, call any Cloudflare API or SDK, write or edit `cert.pem` / `tunnel.state` / `config.yml` / `alias-domains.json` directly.
100
+ **Does not:** drive the Cloudflare dashboard via Playwright, synthesise alternative `cloudflared` flag sequences not in the runbook, call any Cloudflare API or SDK, write or edit `cert.pem` / `config.yml` directly outside the runbook's instructions.
110
101
 
111
- When a script or command fails, the agent reports the failure and cites the relevant recovery step. It does not improvise.
102
+ When a command fails, the agent reports the failure and cites the relevant recovery step. It does not improvise.
@@ -62,7 +62,7 @@ The graph view (at `/graph`) lets you explore the memory directly. Pick a catego
62
62
 
63
63
  ## The Web Interface
64
64
 
65
- The web app runs on your Pi on port 19200. A small always-on front door (`maxy-edge`) owns that port and the remote terminal transport — so when the Software Update command restarts the app server, the browser-side terminal keeps streaming bytes exactly like an SSH session would. The edge also hosts the update flow's own routes (the sudo prompt, the action launcher, the SSE progress stream, the installed-version poll), so the Software Update modal's log panel does not go blank during the app-server restart window — it keeps receiving lines, heartbeats, and the final exit event unbroken. Login cookies are HMAC-signed with a shared key on disk, so both processes recognise the same session without any coordination and you do not have to log in again after an update. Every request is also classified as LAN or external based on the network shape it arrived on — LAN browsers reach admin directly; the remote password screen only appears on the tunnel-exposed admin domain. It provides:
65
+ The web app runs on your Pi on port 19200. A small always-on front door (`maxy-edge`) owns that port. The edge also hosts the `/api/admin/version` route so the HeaderMenu version display keeps reading even during a mid-restart of the brand service. Login cookies are HMAC-signed with a shared key on disk, so both processes recognise the same session without any coordination and you do not have to log in again after an update. Every request is also classified as LAN or external based on the network shape it arrived on — LAN browsers reach admin directly; the remote password screen only appears on the tunnel-exposed admin domain. It provides:
66
66
 
67
67
  - **Admin chat** (at `/`) — your primary interface, PIN-protected
68
68
  - **Public chat** (at `/{agent-name}`) — visitor-facing agents, each with their own URL. On public hostnames, the root path serves the default agent.
@@ -108,31 +108,13 @@ The Data search panel ranks results by combining vector similarity with keyword
108
108
 
109
109
  ## Software Update and Cloudflare Setup
110
110
 
111
- There is no free-form terminal surface in the admin UI ad-hoc shell access stays on SSH. The two flows that used to need a terminal (upgrade, Cloudflare tunnel setup) run as **detached actions** instead.
111
+ Both flows run on the native Claude Code PTY surface in admin chat (Task 287). There is no in-app upgrade modal and no Cloudflare setup form the agent invokes the relevant Bash command directly and its stdout streams into chat verbatim.
112
112
 
113
- **Software Update flow.** Click Upgrade in the modal → the modal asks for your sudo password once → the admin server validates it and launches a transient `systemd-run --user` unit running `npx -y @rubytech/create-maxy@latest`. The action unit is a peer of the brand service (not a child), so the installer's mid-run `systemctl --user restart maxy` does not kill the upgrade itself — it finishes running, then the web UI reloads on the new version. The modal shows a live log panel with three event types:
114
- - every stdout/stderr line the installer emits, timestamped server-side;
115
- - a heartbeat every 5 seconds carrying `state=active` + `last_phase` so you can see the installer is alive even during silent phases (tarball download, dep install);
116
- - a final banner on exit with the return code and elapsed time.
117
-
118
- If the browser drops the SSE connection mid-upgrade (typical during the maxy restart window), the panel reconnects within two seconds and replays any lines you missed from the persisted log.
113
+ - **Software update.** Re-run the installer (`npx -y @rubytech/create-<brand>@latest`) from a shell; HeaderMenu's version row turns sage when `installed === latest`.
114
+ - **Cloudflare setup.** Operator asks in chat; the agent invokes `cloudflared` directly via the Bash tool, following the numbered steps in `plugins/cloudflare/references/manual-setup.md`. cloudflared's stdout and stderr stream into the PTY; the OAuth URL printed by `cloudflared tunnel login` is linkified by the terminal so the operator clicks it and authorises Cloudflare in their own browser.
119
115
 
120
116
  **Mid-turn stream-drop banners.** If a chat turn ends abruptly the bubble shows one of two messages depending on what actually happened. You see "Server is restarting — reconnect will happen automatically." only when the app server itself emits the restart signal — typically during a Software Update or a Cloudflare setup that re-launches the brand service. You see "Lost connection — retrying." when your browser's connection to the Pi dropped mid-stream while the server was still up — typically a flaky Wi-Fi moment or the tunnel hiccupping. Either way the chat resumes once the connection is back; the previously-rendered messages stay on screen so you don't lose context.
121
117
 
122
- **Cloudflare setup flow.** Same pattern — POST to `/api/admin/cloudflare/setup` launches a `cloudflare-setup` action that runs `~/setup-tunnel.sh <brand> <port> <hostname...>`. When the script emits the OAuth consent URL on stdout, the log panel surfaces an **"Authorise in Cloudflare"** button; clicking it opens the consent page in a new tab. After you approve, the script's callback receives `cert.pem` and the setup continues through `tunnel create`/`route`/`run`. On devices where a VNC Chromium is also running, the script can drive the click via CDP automatically (same button remains a harmless safety net). Setup failures return a `CloudflareSetupError` carrying `inputsAlreadyHeld:{admin,public,apex}` and `discoveryResults:{tunnels,domains}` (from the process-lifetime discovery cache `/tunnels` and `/domains` populate on success); the form appends both fields as a fenced JSON block to the chat-relay body so the agent's next reply quotes held values verbatim rather than re-soliciting hostnames.
123
-
124
- **Active-chat stream-log click telemetry.** Clicking "Stream log" in the active chat fetches the URL inline (`/api/admin/logs?type=stream&conversationId=…&download=1`), emits a `[stream-log-click] status=<c> bytes=<n> conversationId=<tail>` line in `server.log` via the `/api/_client-error` event pipe, and downloads the same response. Operator-grep `[stream-log-click] status=404` for client/server identity mismatches.
125
-
126
- **Bundle-mtime session prelude.** Each admin session boot stamps `[boot] bundleMtime=<iso> conversationId=<tail>` in `server.log` next to `[plugins] MCP servers for session:`; the value is consumed by the Cloudflare setup route's retry-decision branch (it compares against the most recent terminal `:Task.completedAt`), never by the LLM. The prior `<deployment>` block was removed in 2026-05-13 doctrine fix because the admin LLM read it as narration permission for internal state; the route now owns the retry decision, the agent restates the literal envelope.
127
-
128
- **Post-action restatement block (2026-05-13 doctrine fix).** `assembleSystemPrompt` injects `<post-action>actionId=<id> outcome=<completed|failed> phase=<phase> at=<iso></post-action>` into the system prompt whenever the dispatcher resolves a terminal `:Task {kind:"cloudflare-tunnel-login"}` on the conversation. Agent-type-agnostic. The IDENTITY clause "If `<post-action>` is present, restate the literal outcome and stop" routes the operator-visible reply. Observability: `[post-action-block] injected|no-prior-action|lookup-failed` log line every turn.
129
-
130
- **Admin reply scrub (2026-05-13 doctrine fix).** Closed-list token scrub at `platform/ui/app/lib/admin-reply-scrub.ts`; tokens `bundle`, `bundleMtime`, `overnight`, `the fix may`, `was patched`, `re-rendering`, `paste the output`, `run the script`, `re-attempt`. Hit records a violation under `llm-narrates-internal-state` (rule_family `extension`) and emits `[admin-reply-scrub] agent=<name> tokens_found=[…] reply_blocked=false`. Future doctrine paragraphs naming a new internal token MUST extend the list in the same PR.
131
-
132
- **Sudo password** is prompted once per upgrade. The admin server pipes it to `sudo -S -v` to validate + cache, then forwards it to the action unit via `systemd-run --setenv=SUDO_PASSWORD` so the installer's in-unit `sudo -S` reads it directly — per-TTY sudoers configurations where the user-level cache does not cover a fresh systemd-run unit still work. The password is never written to any log, SSE frame, or persisted file.
133
-
134
- **Log files.** Each action writes its full output to `~/.maxy/logs/actions/<actionId>.log` for seven days. `journalctl --user --identifier=maxy-action-<actionId>` gives the systemd-level view.
135
-
136
118
  **Authorisation** is inherited from the same `canAccessAdmin()` gate that wraps every `/api/admin/*` route.
137
119
 
138
120
  ## AI Content Provenance
@@ -152,7 +152,7 @@ After this, every `console.error("[your-tool]...")` from any tool in the plugin
152
152
 
153
153
  **How the tee decides which file to write to:** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
154
154
 
155
- **`STREAM_LOG_PATH` reaches every Claude Code child.** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh`, `reset-tunnel.sh`, and `list-cf-domains.sh` under `platform/plugins/cloudflare/scripts/` — read the variable, guard against a missing value with a loud exit, and tee subprocess output line-by-line into the same per-conversation file. Each spawn writes one `[spawn-env] STREAM_LOG_PATH=set pid=… conversationId=… site=…` line so the env-propagation is auditable per session. The chat UI tails the same file for lines matching `^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] ` any lowercase scope shape participates on first write (earlier platform fixes generalised from the pre-592 enum `setup-tunnel|reset-tunnel`) and emits them as `script_stream` SSE events; see `.docs/web-chat.md` for the contract. Inner-layer helpers that a.sh wrapper spawns (e.g. `list-cf-domains.ts` via `node --experimental-strip-types`) must write phase lines directly to `STREAM_LOG_PATH` rather than relying on stderr propagation; the build-gate `platform/ui/scripts/check-stream-log-contract.mjs` enforces this and is the definitive reference for the three allowed patterns (tee-wrapped, direct-write, or explicit stderr-only marker).
155
+ **Bash commands stream straight into the PTY.** Maxy Code's admin and public chat run on the native Claude Code PTY (Task 287). The per-conversation server-side stream log that the retired web-UI dispatcher tailed is gone; agent-invoked Bash commands (including direct `cloudflared` invocations for Cloudflare setup Task 288) print their stdout and stderr directly, and the PTY renders the output in chat verbatim.
156
156
 
157
157
  **Retrieve MCP diagnostic lines for a conversation:**
158
158
 
@@ -184,325 +184,13 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
184
184
 
185
185
  ---
186
186
 
187
- ## Action runner — upgrade or Cloudflare setup appears stuck
188
187
 
189
- replaced the ttyd/xterm admin terminal with a detached action runner. Upgrades and Cloudflare setup now run under transient `systemd-run --user` units whose stdout+stderr land in a persisted per-action log, streamed to the browser via SSE. An earlier fix moved the four routes that serve the modal (`/api/admin/actions/*`, `/api/admin/version`) onto `maxy-edge.service`, so the log panel's stream survives a mid-run restart of `maxy.service` without reconnecting.
188
+ ## Software update and Cloudflare setup
190
189
 
191
- **"Connection lost reconnecting…" banner appears during an upgrade.** On post-Task-666 bundles this should never appear while the upgrade is in flight the routes live on the always-on edge. If you see the banner during steps 8→11 of an upgrade on a current bundle, it is a **regression**, not expected behaviour: the routes have drifted back onto `maxy.service` or the edge's Hono dispatcher is not intercepting them. Check `~/.maxy/logs/edge.log` for `[edge-admin]` entries during the window; absence means the edge never received the request. The prebuild gate `platform/ui/scripts/check-edge-admin-routes.mjs` exists specifically to catch this drift before it ships.
190
+ Both flows run on the native Claude Code PTY surface in admin chat (Task 287). The retired action-runner / terminal-modal troubleshooting sections that lived here have been removed because those surfaces no longer exist; failures now manifest as plain stderr from the agent-invoked Bash command, visible in chat.
192
191
 
193
- **Agent does not respond to any message after a fresh install or restart.** The chat ingress quartet surfaces where the request silently stopped. From admin chat, ask the agent to read the chat-ingress timeline for the last 5 minutes — internally it runs `logs-read.sh --tail chat-attempts 5`, which cross-greps `edge.log` and `server.log` for the four checkpoints (edge inbound, admin-auth verdict, handler entry, edge outcome). Any missing row localises the failure: no edge inbound = request never reached the device (network, DNS, or cloudflare tunnel); inbound present + no auth verdict = handler unreachable (Hono routing regression); auth verdict = reject + a `code=` value the client banner matches; auth = accept + no handler entry = post-auth routing regression; all three rows present + no edge outcome = upstream crash or socket abort. See the developer doc `.docs/web-chat.md#chat-ingress-quartet` for the full state-machine and the prefix taxonomy.
194
-
195
- **Heartbeat stalled** (log panel header shows rising `silent Ns` amber badge).
196
-
197
- - Open the log panel header: `state: <systemd_state>` tells you the unit's current state.
198
- - `systemd_state: active` + silent >30s → the child is running but emitting nothing. Expected for `npx` while it downloads the tarball, or `cloudflared tunnel login` waiting for an operator click.
199
- - `systemd_state: inactive` + no `exit` event → the exit event was missed; the server-side heartbeat timer will emit it on the next 5 s tick.
200
- - `systemd_state: failed` → see the next symptom.
201
-
202
- **`ActiveState=failed`** (log panel's exit banner shows a non-zero code).
203
-
204
- - Read the persisted log directly: `~/.maxy/logs/actions/<actionId>.log` (or `.realagent/...`) has every stdout+stderr line the unit emitted.
205
- - `journalctl --user --identifier=maxy-action-<actionId>` shows systemd's own record including ExecStartPre/ExecStopPost if any.
206
- - Common cases:
207
- - Wrong sudo password → `sudo: 1 incorrect password attempt` near the top of the log; re-open the upgrade modal, enter the correct password.
208
- - Network failure during `npx` → `npm ERR! network` lines; re-open the modal and retry when network is restored.
209
- - `cloudflared tunnel login` timed out waiting for OAuth → action exits non-zero with `Timed out after Ns waiting for cert.pem`; re-trigger from the Cloudflare setup form.
210
-
211
- **"Authorise in Cloudflare" button never appears** (cloudflare-setup action).
212
-
213
- The setup script emits either the raw `https://dash.cloudflare.com/argotunnel?...` URL in cloudflared's own stderr OR an explicit `OAUTH_URL: <url>` stdout line once URL extraction succeeds. The log panel's regex matches either. If neither appears within ~15 s of launch:
214
-
215
- - The action log file (`~/.maxy/logs/actions/<actionId>.log`) should show `[script:setup-tunnel:cloudflared]` lines. No such lines → cloudflared isn't spawning (check whether the binary is on PATH in the transient unit; `systemctl --user show maxy-action-<actionId>` reveals the environment).
216
- - Lines present but no URL → cloudflared output-format drift; file a task with the last 20 lines of the action log.
217
-
218
- **Log file missing (action stream returns 404).**
219
-
220
- The transient unit was auto-collected by systemd before the client subscribed. Race condition: action finished in <1 s. The per-action log file is retained for 7 days; look for it by name under `~/.maxy/logs/actions/`. If it isn't there, the unit failed before any output (check `journalctl --user -u maxy-action-<id>`).
221
-
222
- **Cloudflare-setup action card shows "Failed (exit null)" after the tunnel works.**
223
-
224
- > **Pre-Task-860 misdiagnosis.** Devices on platform versions misrender a successful `cloudflare-setup` run as red `Failed (exit null) · ~20s` because the script-armed brand-service restart kills the SSE generator before it can read the action's exit code. The Cloudflare side is fine in this case — the tunnel is created, DNS is routed, the brand service comes back up.
225
- >
226
- > **First-line check:** open the persisted action log directly. Two markers prove success:
227
- >
228
- > ```bash
229
- > grep -E 'step=service-restart-armed exit=0|step=done' \
230
- > ~/.{configDir}/logs/actions/cloudflare-setup-*.log | tail -2
231
- > ```
232
- >
233
- > Both lines present in the log ⇒ the script succeeded; the UI banner is the bug, not the run. On post-Task-860 platforms the action card renders `Completed · Ns` on the same log shape; the card now distinguishes four states:
234
- >
235
- > - `Completed` (green) — `code === 0` (whether reported by systemd or recovered from the log).
236
- > - `Failed (exit N)` (red) — non-zero exit reported by systemd. Real failure, follow the existing remediation paths.
237
- > - `Restart in progress` (neutral sage) — arming line present in log but the post-arm `step=done` was not written before the unit was GC'd. Resolves automatically once `/api/admin/version` responds; if it stays >90 s, falls through to amber.
238
- > - `Failed (exit unobserved)` (amber) — neither systemd nor the log can confirm a terminal state. Check `journalctl --user -u maxy-action-<id>`; the unit failed early or the log was truncated.
239
- >
240
- > The server emits `[action-runner] reconcile actionId=<id> result=<succeeded|failed|restart-in-progress|unresolved> source=<systemd|persisted-log> ms=<n>` once per terminal-state resolution. Missing reconcile line on a finished run = stale platform; upgrade.
241
-
242
- **Cloudflare-setup auto-relays "completed" but the next chat turn 502s.**
243
-
244
- The current contract is a single client-driven resume: when the setup script exits cleanly, the form fires a resume event, the chat hook waits for `/api/health` to fail then succeed (the brand-service down-then-up cycle), re-binds the conversation to the new server process, and sends the "completed" marker as a normal hidden chat POST that re-invokes the agent in the operator's real session. No relay queue, no boot-drain, no banner.
245
-
246
- Diagnostic recipe:
247
-
248
- ```
249
- grep '\[admin-resume\] reason=post-restart' ~/.maxy/logs/server.log | tail # /resume server-side
250
- grep '\[session\] cookie-bridge accountId=' ~/.maxy/logs/server.log | tail # session re-hydrated
251
- grep '\[persist\] .* role=user .* Cloudflare setup completed' ~/.maxy/logs/server.log | tail
252
- grep '\[client-event\] kind=post-restart-resume' ~/.maxy/logs/server.log | tail
253
- ```
254
-
255
- Failure modes:
256
- - `[client-event] kind=post-restart-resume phase=start` absent: form's CustomEvent never reached the chat hook (regression in form `onExit` or chat-listener mount).
257
- - `[client-event] phase=start` present, `[admin-resume] reason=post-restart` absent: `waitForRestartCycle` exhausted its bound (brand never restarted) or `/resume` rejected — check `phase=health-timeout` / `phase=resume-rejected`.
258
- - All four present except `[persist] role=user … Cloudflare setup completed`: marker chat POST failed — check the chat surface for an inline error or a `phase=resume-error` line.
259
- - Refresh after a successful Cloudflare setup shows a visible `Cloudflare setup completed (actionId: …)` user bubble: the resume mappers' synthetic-marker suppression broke or the marker shape drifted. Server-side `[admin-resume] syntheticHidden=<n>` field on the same line counts user rows the client should hide (`_componentDone` envelopes, `_lifecycle` envelopes, and the `Cloudflare setup completed (actionId: …)` literal) — a count of 0 against a refresh that should have hidden one means the helper at `platform/ui/app/lib/synthetic-marker.ts` no longer recognises the producing literal (check `CloudflareSetupForm.tsx` and `useAdminChat.ts` for shape drift).
260
-
261
- ---
262
-
263
- ## Software Update click shows an error instead of opening the terminal
264
-
265
- > **First-line diagnostic for the byte-stream xterm.js terminal surface:** `sudo systemctl --user status maxy-ttyd` plus `sudo grep 'ttyd-proxy' ~/.maxy/logs/edge-boot.log | tail -20`. Failure mode signals: `ttyd-ws-upgrade accepted` with no `ttyd-proxy-open` → `maxy-ttyd.service` is down; `ttyd-proxy-open` with no `ttyd-proxy-chunk dir=upstream→client` → ttyd/tmux is not attaching a PTY.
266
-
267
- **Symptom:** You clicked **Upgrade** in the Software Update modal, but instead of the VNC terminal overlay appearing, the modal shows a red error row like:
268
-
269
- ```
270
- [terminal-launch] failed err="window absent from target display after spawn" pid=1234 display=:99 observed_windows=0 transport=vnc reason=upgrade
271
- ```
272
-
273
- …and the Upgrade button re-labels to "Try again".
274
-
275
- **What it means:** `POST /api/admin/terminal/launch-upgrade` returned a 502 — the admin server tried to spawn a real terminal on the VNC display `:99` with `npx -y @rubytech/create-maxy@latest` pre-loaded, but the spawn failed or the window never appeared on the target display. The modal's upgrade path uses the same pipeline as the header-menu Terminal; if the operator Terminal button works, the VNC stack itself is healthy and the fault is usually a missing dep or a stale binary. If the operator Terminal also fails, start with the "Header Terminal click shows an error alert" section below.
276
-
277
- Step-by-step diagnosis — the error string in the modal is identical to the entry in `~/.maxy/logs/vnc-boot.log`, so you can grep it directly:
278
-
279
- ```bash
280
- # 1. Find the latest upgrade launch attempt
281
- sudo grep 'action=launch-upgrade' ~/.maxy/logs/vnc-boot.log | tail -10
282
-
283
- # 2. See the script-level failure line (same string the modal shows)
284
- sudo tail -n 30 ~/.maxy/logs/terminal-launch.log
285
-
286
- # 3. Verify binaries are present
287
- which xterm xdotool
288
-
289
- # 4. Confirm the VNC display itself is up
290
- DISPLAY=:99 xdpyinfo >/dev/null 2>&1 && echo "display:99 ok" || echo "display dead"
291
- ```
292
-
293
- **Common upgrade-specific failures:**
294
-
295
- - `err="no terminal emulator installed"` → run `sudo apt-get install -y xterm xdotool` or re-run `npx -y @rubytech/create-maxy@latest` — the installer provisions both as hard deps.
296
- - `err="spawn detached but no terminal PID visible within 1s"... reason=upgrade` → X server on `:99` is wedged. the VNC stack is owned by `maxy-edge`, so the recovery is `sudo systemctl --user restart maxy-edge` (cycles `vnc.sh start` via that unit's ExecStartPre). Then click **Try again** in the modal.
297
- - `err="window absent from target display after spawn"... reason=upgrade` → the spawn succeeded but landed on the wrong display (earlier platform fixes class). Re-run the installer to refresh the `resolve_terminal_bin` logic. If a stale `gnome-terminal` is hitting `:99` via D-Bus delegation, the installer's `xterm` fallback is the fix.
298
- - `err="xdotool not installed — re-run installer to repair"` → earlier platform fixes preflight. `sudo apt-get install -y xdotool` or re-run the installer.
299
- - `err="VNC failed to start after recovery attempt"` → `Xtigervnc` itself is not coming up. Inspect `~/.maxy/logs/vnc-boot.log` for tigervnc startup lines; `ss -ltn '(sport = 6080 or sport = 5900)'` should show both ports listening.
300
-
301
- **Click Try again** after applying any fix — the modal re-POSTs the launch-upgrade request.
302
-
303
- ---
304
-
305
- ## Upgrade terminal stays black after clicking Upgrade (VNC WebSocket rejected)
306
-
307
- **Symptom:** The launch-upgrade POST returns 200 (the server-side spawn succeeded), but the browser's VNC iframe shows a black rectangle and the DevTools console spins on `WebSocket connection to '/websockify' failed` / `Connection closed (code: 1006)` every few seconds.
308
-
309
- **What it means:** The browser has a session cookie the `/websockify` auth gate refuses to accept., every rejection logs three fields that identify the failing layer:
310
-
311
- ```bash
312
- sudo tail -200 ~/.maxy/logs/vnc-boot.log | grep 'decision="rejected"'
313
- ```
314
-
315
- - `cookieHeaderPresent=false` → the browser never sent a cookie. Check the iframe context / SameSite policy / cookie domain. Most commonly: the user opened admin over HTTP instead of HTTPS, or the cookie is scoped to a different subdomain.
316
- - `cookieHeaderPresent=true tokenPresent=false` → the cookie is present but the server rejects its signature. This is either tampering or a corrupted/missing shared secret. Check that `~/.maxy/credentials/remote-session-secret` exists on disk (mode `0600`, 64 hex chars) and has not been recently overwritten.
317
- - `tokenExpired=true` → the 24h session TTL elapsed. Log in again.
318
-
319
- If the secret file is missing entirely, re-run `npx -y @rubytech/create-maxy@latest` — the installer provisions it idempotently, preserving an existing file on upgrade and creating a fresh one on first install.
320
-
321
- ---
322
-
323
- ## Upgrade terminal opens but npx never runs
324
-
325
- **Symptom:** You clicked **Upgrade**, the VNC overlay opened with a visible shell prompt, but `npx -y @rubytech/create-maxy@latest` does not execute and the shell is idle.
326
-
327
- **What it means:** The VNC spawn succeeded but the binary-specific command dispatcher (xterm `-e` or gnome-terminal `--`) did not forward the command. Given the installer ships `xterm` on `:99` by default (with D-Bus-safe dispatching), the likely causes are:
328
-
329
- - A stale binary override has re-pointed `resolve_terminal_bin` to `gnome-terminal` on `:99` despite the fix.
330
- - A shell the operator manually spawned (via the header Terminal) wasn't killed before the upgrade click, and `ensureTerminalUpgrade`'s pre-kill step silently failed.
331
-
332
- **Check:**
333
-
334
- ```bash
335
- # The spawned binary must appear in this log with the bash-c wrapper
336
- sudo grep 'started.*reason=upgrade' ~/.maxy/logs/terminal-launch.log | tail -3
337
- # Expected shape: started pid=<N> display=:99 cmd="/usr/bin/xterm... -e bash -c 'npx -y @rubytech/create-maxy@latest; exec bash'" transport=vnc windowPresent=true reason=upgrade
338
- ```
339
-
340
- If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the vnc.sh on the device is stale. If the command IS logged correctly but nothing is running, open the VNC overlay and type `history | tail` inside the shell — if the npx line is there, it ran and exited (check `~/.maxy/logs/install-*.log` for the exit status).
341
-
342
- ---
343
-
344
- ## Header Terminal click shows an error alert
345
-
346
- **Symptom:** You opened the burger menu and clicked **Terminal**, but instead of the overlay appearing you got an inline error like "Terminal failed to start" or "VNC failed to start".
347
-
348
- **What it means:** `POST /api/admin/terminal/launch` returned a 502 — either the VNC stack on port 5900 is down or the terminal emulator could not be spawned on display `:99`. This is a VNC-stack or display-spawn failure.
349
-
350
- Step-by-step diagnosis:
351
-
352
- ```bash
353
- # 1. Check the terminal-launch log for the specific failure reason
354
- sudo tail -n 50 ~/.maxy/logs/terminal-launch.log
355
-
356
- # 2. Check the node-side state machine (ensure-terminal entries)
357
- sudo grep ensure-terminal ~/.maxy/logs/vnc-boot.log | tail -20
358
-
359
- # 3. Verify the VNC display itself is healthy
360
- sudo ~/maxy/platform/scripts/vnc.sh status # should print "running"
361
- DISPLAY=:99 xdpyinfo >/dev/null 2>&1 && echo "display ok" || echo "display dead"
362
-
363
- # 4. Confirm a terminal binary is installed (xterm is the always-available fallback)
364
- which gnome-terminal; which xterm
365
- ```
366
-
367
- Most common failures and fixes:
368
-
369
- - `[terminal-launch] failed err="no terminal emulator installed"` → run `sudo apt-get install -y xterm xdotool` (or re-run the installer, which installs both as dependencies).
370
- - `[terminal-launch] failed err="spawn detached but no terminal PID visible within 1s"` → X server on `:99` is wedged. `sudo systemctl --user restart maxy-ui` cycles the VNC stack via `vnc.sh start`.
371
- - `[terminal-launch] failed err="window absent from target display after spawn" pid=<N> display=:99 observed_windows=0` → the spawned emulator's window landed on the wrong display (earlier platform fixes — almost always a stale gnome-terminal binary on an upgraded install; the fix ships `xterm` as the `:99` default but a stale `resolve_terminal_bin` on an old bundle can still trigger it). Check `which gnome-terminal xterm xdotool` — all three should exist; if not, re-run the installer. Also check `DISPLAY=:99 xdotool search --onlyvisible --class '.'` manually: empty output confirms no window on `:99`; a non-empty result after a fresh click means the window IS there and the check itself is wrong (file an issue).
372
- - `ensure-terminal action="escalate-vnc-restart"` followed by `degraded` → `Xtigervnc` itself is not coming up. Check `~/.maxy/logs/vnc-boot.log` for the tigervnc startup lines.
373
-
374
- ## Terminal fails after upgrade — `Invalid response` or `window absent from target display`
375
-
376
- **Symptom:** You upgraded a {{productName}} device and the header-menu **Terminal** click now fails — overlay shows `Invalid response`, or `terminal-launch.log` shows `[terminal-launch] failed err="window absent from target display after spawn" pid=<N> display=:99 observed_windows=0 transport=vnc cmd="/usr/bin/xterm "`. The terminal worked on the previous version and the upgrade reported success.
377
-
378
- **What it means:** The installer declared a new apt dep between versions (e.g. `xdotool` in 1.0.667), but on your device the installer's apt step was silently skipped because `sudo` requires a password and the installer could not escalate non-interactively. The declared dep was never installed, and `vnc.sh check_window_on_display` then either failed to find the binary (exit 127 now surfaces this as a distinct `failed err="xdotool not installed — re-run installer to repair"` line) or reached the `window absent` path because its preflight tool was missing. See `.docs/deployment.md` ("sudo interactivity contract") for the full mechanism.
379
-
380
- **Check:**
381
-
382
- ```bash
383
- dpkg -s xdotool 2>&1 | head -2 # should report "Status: install ok installed"
384
- which xdotool # should return /usr/bin/xdotool
385
- ```
386
-
387
- **Fix — two options:**
388
-
389
- 1. **Re-run the installer from an interactive shell** (preferred on upgrade — picks up any future missing deps too):
390
-
391
- ```bash
392
- npx -y @rubytech/create-maxy@latest
393
- ```
394
-
395
- The installer now probes `dpkg -s` for every declared apt dep and either prompts you for your sudo password to install missing ones, or loud-fails non-interactively with the exact `sudo apt-get install -y <list>` command to repair. It will no longer print `Skipping apt-get (deps assumed present from prior install)` and continue as if green.
396
-
397
- 2. **Repair manually** (faster if you only want the one binary):
398
-
399
- ```bash
400
- sudo apt-get install -y xdotool
401
- ```
402
-
403
- Then click **Terminal** again — it should spawn within a second and the overlay should render the xterm.
404
-
405
- **Regression boundary:** if you see `[terminal-launch] failed err="xdotool not installed — re-run installer to repair"` in `~/.maxy/logs/terminal-launch.log`, the vnc.sh preflight is telling you the binary is missing before spawn — same class as above, distinct from the earlier platform fixes `window absent from target display after spawn` string. The two strings never appear for the same click.
406
-
407
- ---
408
-
409
- ## Installer aborts on Ubuntu 24.04 with `dpkg -s: chromium`
410
-
411
- **Symptom:** On a fresh (or re-run) `npx -y @rubytech/create-maxy@latest` on an Ubuntu 24.04 Noble laptop/desktop, step 1/12 aborts:
412
-
413
- ```
414
- [1/12] System dependencies and network...
415
- Missing apt deps (1): chromium
416
- apt install (VNC stack): tigervnc-standalone-server python3-websockify novnc xdg-utils chromium xterm xdotool
417
- > sudo apt-get install -y... chromium...
418
- OK in 0.3s
419
- [ERROR] Setup failed: apt-get install (VNC stack) returned 0 but packages are still not installed per dpkg -s: chromium
420
- ```
421
-
422
- Directly probing the device shows `dpkg -s chromium` non-zero, `apt-cache policy chromium` reports `Candidate: (none)`, but `/usr/bin/chromium` exists and works, and `snap list chromium` shows the snap is installed.
423
-
424
- **What it means:** You are on a **pre-637 installer**. On Noble, `chromium` is a virtual package aliasing to `chromium-browser` — `apt-get install chromium` succeeds silently but nothing named `chromium` ever lands in dpkg's state DB. the post-install probe (correctly) notices the discrepancy and aborts. An earlier fix added alias resolution (`chromium` → `chromium-browser`) so the probe matches the dpkg-recorded name and passes.
425
-
426
- **Fix:** Upgrade to a post-637 version of `@rubytech/create-maxy`:
427
-
428
- ```bash
429
- npx -y @rubytech/create-maxy@latest
430
- ```
431
-
432
- **Manual bypass** (if you must install before 637 publishes, or if you're on a restricted shell): pre-install the real.deb name and re-run the installer.
433
-
434
- ```bash
435
- sudo apt-get install -y chromium-browser
436
- npx -y @rubytech/create-maxy@latest
437
- ```
438
-
439
- The installer's resolver will then see `dpkg -s chromium-browser` exit 0 for the alias target, filter `chromium` out of the missing set, and skip the apt-install branch entirely. Raspberry Pi OS (Debian 12 Bookworm) is unaffected — `chromium` IS a concrete.deb on Bookworm.
440
-
441
- **Deeper diagnostic**: post-637 installer logs carry the full classification in the error message itself — `(resolved-name=<name>, apt-cache-policy=<summary>, command-v=<path>, snap-status=<summary>)`. If you see that longer form and still fail, the apt state itself is inconsistent (snap removed, `chromium-browser` uninstalled after previous install) — repair with `sudo apt-get install --reinstall -y chromium-browser`.
442
-
443
- ---
444
-
445
- ## VNC browser will not start on Linux laptop — `Permission denied (13)` on SingletonLock
446
-
447
- **Symptom:** Boot log `~/.{brand}/logs/vnc-boot.log` shows:
448
-
449
- ```
450
- Starting Chromium on :<vncDisplay> (vnc) profile=/home/<user>/.{brand}/chromium-profile CDP=:<cdpPort>
451
- ERROR:chrome/browser/process_singleton_posix.cc:345] Failed to create
452
- /home/<user>/.{brand}/chromium-profile/SingletonLock: Permission denied (13)
453
- ERROR: Chromium failed to start on :<vncDisplay> (vnc) — CDP port <cdpPort> not listening (browser-specialist degraded)
454
- ```
455
-
456
- `dmesg` (or `journalctl -k`) shows AppArmor `DENIED` lines naming `profile="snap.chromium.chromium"` and the `~/.{brand}/chromium-profile/` path. The brand admin chat reports the public-agent VNC browser as unavailable.
457
-
458
- **What it means:** Your `/usr/bin/chromium` is a snap symlink. Snap's AppArmor profile excludes hidden top-level paths under `$HOME` from its `home` interface, so writes to per-brand Chromium profile dirs at `~/.maxy/chromium-profile/` and `~/.realagent/chromium-profile/` are denied — Chromium cannot create its `SingletonLock` and never starts the CDP listener. This is exclusively a Linux-laptop problem (Ubuntu Noble); Raspberry Pi OS Bookworm ships `chromium` as a real `.deb` and is unaffected.
459
-
460
- **Fix:** Re-run the installer at version 1.0.849 or later. The installer detects the snap-confined chromium during system-dependency setup and replaces it with Google Chrome stable from Google's signed apt repo:
461
-
462
- ```bash
463
- npx -y @rubytech/create-maxy@latest
464
- ```
465
-
466
- The resolved non-snap binary path is recorded at `<INSTALL_DIR>/platform/config/chromium-binary.path` (single line) and read by every Chromium call site (VNC service, in-page wrapper, Playwright server). After re-running, verify with the bundled acceptance script:
467
-
468
- ```bash
469
- MAXY_PLATFORM_ROOT=$HOME/<install-dir>/platform $HOME/<install-dir>/platform/scripts/test-laptop-vnc-boot.sh
470
- ```
471
-
472
- The script exits 0 only when (1) the configured Chromium realpath is non-snap, (2) the path is absolute and executable, (3) the per-brand CDP port returns Chromium version JSON, and (4) `vnc-boot.log` since the last `[vnc.sh] start` ends with `VNC + browser stack running` and contains no `Chromium failed to start` line.
473
-
474
- **Deeper diagnostic:** `vnc.sh` and the in-page wrapper now refuse to start (and exit with a snap-Chromium reference in the boot log) when `chromium-binary.path` is absent or its realpath lands under `/snap/`. If you see those messages, the install completed before the fix shipped — re-run `npx -y @rubytech/create-maxy@latest`. Manual workaround for an emergency (not a fix): `sudo apt-get install -y google-chrome-stable` and confirm `which google-chrome-stable` is non-snap, then re-run the installer to write `chromium-binary.path`.
475
-
476
- ---
477
-
478
- ## Terminal iframe renders black and cursor vanishes over the canvas
479
-
480
- **Symptom:** Header-menu Terminal click appears to succeed — no error toast, overlay opens — but the iframe renders uniformly black, keystrokes do not reach any shell, and the mouse cursor disappears the moment it enters the iframe (visible elsewhere in the page).
481
-
482
- **What it means:** This is the exact symptom class that earlier platform fixes closed. Before 632, `/usr/bin/gnome-terminal` would receive `DISPLAY=:99` but D-Bus-delegate the window-create request to the session's `gnome-terminal-server`, which opens the window on `:0` (the operator's physical screen) instead. The iframe, rendering `:99`, has no window to show — hence the black canvas, dead keystrokes, and null-cursor (noVNC's Cursor pseudo-encoding renders `cursor: none` when the server sends no cursor). If you are seeing this *after* earlier platform fixes shipped, the installer did not run on this device or a stale bundle is in place.
483
-
484
- Step-by-step diagnosis:
485
-
486
- ```bash
487
- # 1. Check terminal-launch.log for the window-absent failure
488
- sudo grep 'window absent from target display' ~/.maxy/logs/terminal-launch.log | tail -5
489
-
490
- # 2. Verify the expected binaries are installed
491
- which xterm xdotool # both must exist — installer adds them
492
-
493
- # 3. Confirm the VNC display is serving the iframe
494
- DISPLAY=:99 xdpyinfo >/dev/null 2>&1 && echo "display:99 ok"
495
- DISPLAY=:99 xdotool search --onlyvisible --class '.' # non-empty means windows ARE on:99
496
-
497
- # 4. Check which binary resolve_terminal_bin chose
498
- grep 'cmd=' ~/.maxy/logs/terminal-launch.log | tail -3
499
- # Expected for remote-origin clicks: cmd="/usr/bin/xterm"
500
- # If you see: cmd="/usr/bin/gnome-terminal --wait" on:99 → stale bundle, re-run installer
501
- ```
502
-
503
- If the failure log shows `window absent from target display`, the fix already ran but spawn still went to the wrong display — re-run the installer (`npx -y @rubytech/create-maxy@latest`) from a shell to pick up the latest `xterm` + `xdotool` preflight. If the failure log shows no recent entries and the iframe is still black, check `ss -ltn '(sport = 6080 or sport = 5900)'` and `pgrep -af Xtigervnc|websockify` — the symptom might be a VNC-stack regression rather than a terminal-binary mismatch.
504
-
505
- Both the header Terminal and the Software Update modal's Upgrade button now share the same VNC spawn pipeline, so a failure in one usually reproduces in the other — if the header Terminal opens a blank shell cleanly but the Upgrade modal errors, the problem is upgrade-specific (see "Software Update click shows an error" above).
192
+ - **Software update.** Re-run `npx -y @rubytech/create-<brand>@latest` from a shell; if the installer fails, its stdout is the diagnostic record. HeaderMenu turns sage when `installed === latest`.
193
+ - **Cloudflare setup.** The agent invokes `cloudflared` directly via Bash, following the cloudflare plugin's `references/manual-setup.md`. Failures surface as cloudflared's literal stderr plus a non-zero exit. Recovery paths live in `references/reset-guide.md` and `references/manual-setup.md`.
506
194
 
507
195
  ## Orphan Account Directory Archived to `.trash/`
508
196
 
@@ -23,13 +23,44 @@ Outcome (binding):
23
23
 
24
24
  Constraint (binding): **No downstream venture-studio skill (`brand-pack`, `zero-to-prototype`, `investor-data-room` Stages 3+, etc.) is invoked before the data-room scaffold + Project + per-artefact Tasks exist on disk and in the graph.** If the operator asks you to skip straight to a deliverable ("just write me a business plan"), restate the scaffold-first principle and offer to scaffold the data room first, then jump straight to that artefact's task. The principle is the outcome contract; the scaffold proves it.
25
25
 
26
+ ### Deterministic pre-flight gate
27
+
28
+ Before invoking any downstream skill, both checks below must pass. Both are deterministic — exit-code on `ls`, structured output on `project-list`. Neither check depends on LLM judgement.
29
+
30
+ 1. **Directory check:** `[ -d "${PROJECT_ROOT}/.docs/data-room/01-narrative" ]` returns exit 0.
31
+ 2. **Graph check:** the `project-list` tool returns a Project whose `name` matches the business's working name.
32
+
33
+ If either check fails, run the scaffolding script (see below) before the skill fires. If the script fails, surface the literal stderr error to the operator — do not narrate around it (memory: [[feedback_no_stdout_parsing_for_control_flow]]).
34
+
35
+ ### Scaffolding script
36
+
37
+ `bin/scaffold.sh` is the deterministic enforcement layer. It creates the Project + eight artefact Tasks (via the work plugin's `project-create-cli` bridge) and materialises the ten-section directory tree. Graph write first, directories second; idempotent re-run if any partial state lands.
38
+
39
+ ```bash
40
+ ACCOUNT_ID="${ACCOUNT_ID}" \
41
+ premium-plugins/venture-studio/bin/scaffold.sh \
42
+ "${PROJECT_ROOT}" \
43
+ "${BUSINESS_NAME}"
44
+ ```
45
+
46
+ The script is the binding check, not the prose principle above. The prose explains why; the script is what makes it true.
47
+
26
48
  ## First-conversation routing
27
49
 
28
- When the operator first invokes this agent, confirm the intent ("Are we founding a new business, or working on an existing one?") and run the scaffolding sequence:
50
+ When the operator first invokes this agent, confirm the intent ("Are we founding a new business, or working on an existing one?") and run the scaffolding script — it does steps 1–3 below in a single atomic invocation:
51
+
52
+ ```bash
53
+ ACCOUNT_ID="${ACCOUNT_ID}" \
54
+ premium-plugins/venture-studio/bin/scaffold.sh \
55
+ "${PROJECT_ROOT}" \
56
+ "${BUSINESS_NAME}"
57
+ ```
58
+
59
+ The script's three effects:
29
60
 
30
- 1. **Scaffold the data room.** Invoke the `investor-data-room` skill Stage 2 — it creates the ten-section graph under the operator's project root (`<project-site>/.docs/data-room/`). The tree is named upfront so every artefact has a slot to land in.
31
- 2. **Create the `Project` node.** Use the `projects` plugin's `project-create` tool with tier `full` (data-room work is multi-phase). Project name is the business's working name; description references the data-room root path. The `project-create` call accepts a work-items list — pre-seed it with the artefact tasks below in one transaction.
32
- 3. **Enumerate Tasks per artefact**, one per data-room section / output document, in section order. Suggested task list (the `project-create` tool accepts these as `workItems`):
61
+ 1. **Scaffolds the data room.** Materialises the ten-section directory tree under `<project-root>/.docs/data-room/`. Every artefact has a slot to land in before any artefact is produced.
62
+ 2. **Creates the `Project` node.** Tier `full` (data-room work is multi-phase). Project name is the business's working name; description references the data-room root path.
63
+ 3. **Enumerates the eight artefact Tasks** in section order one transaction, atomic with the Project. The work-item list the script pre-seeds:
33
64
  - **Stage 1 — Office-hours design doc** → produces `01-narrative/office-hours-design.md` (skill: `office-hours`)
34
65
  - **Brand pack** → produces brand identity files into `06-product-ip/brand/` (skill: `brand-pack`)
35
66
  - **Stage 2 — Wedge validation + landing page + PRD** → produces `01-narrative/{PMF, LANDING, PRD}.md` (skill: `zero-to-prototype`)