@rubytech/create-maxy-code 0.1.108 → 0.1.109

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/package.json +1 -1
  2. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
  3. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +12 -12
  4. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +9 -43
  5. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +27 -29
  6. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +14 -9
  7. package/payload/platform/plugins/docs/references/platform.md +4 -22
  8. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  9. package/payload/platform/plugins/docs/references/troubleshooting.md +4 -316
  10. package/payload/platform/plugins/venture-studio/PLUGIN.md +35 -4
  11. package/payload/platform/plugins/venture-studio/bin/scaffold.sh +104 -0
  12. package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +3 -1
  13. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts +2 -0
  14. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts.map +1 -0
  15. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js +78 -0
  16. package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js.map +1 -0
  17. package/payload/platform/scripts/vnc.sh +4 -3
  18. package/payload/premium-plugins/venture-studio/PLUGIN.md +35 -4
  19. package/payload/premium-plugins/venture-studio/bin/scaffold.sh +104 -0
  20. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +3 -1
  21. package/payload/server/{chunk-AGFS3TVN.js → chunk-FSPPVVWM.js} +1303 -496
  22. package/payload/server/maxy-edge.js +21 -259
  23. package/payload/server/public/assets/{ChatInput-DJsqm_Gf.js → ChatInput-CsnIedhS.js} +1 -5
  24. package/payload/server/public/assets/{Checkbox-DGZG9BKc.js → Checkbox-CWugFyFT.js} +1 -1
  25. package/payload/server/public/assets/admin-D3gZuyUn.js +217 -0
  26. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Dw16BhiX.js → architectureDiagram-Q4EWVU46-CeTRKWDb.js} +1 -1
  27. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DIOpmf5Y.js → blockDiagram-DXYQGD6D-DeeIX5U3.js} +1 -1
  28. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Tdb_HZeX.js → c4Diagram-AHTNJAMY-CFsqZuil.js} +1 -1
  29. package/payload/server/public/assets/channel-CrSx5mnG.js +1 -0
  30. package/payload/server/public/assets/{chunk-336JU56O-CebpwDDe.js → chunk-336JU56O-DpIXuFM0.js} +2 -2
  31. package/payload/server/public/assets/{chunk-426QAEUC-BtRCmfDU.js → chunk-426QAEUC-Qk8qqrvA.js} +1 -1
  32. package/payload/server/public/assets/{chunk-4TB4RGXK-BZ3GEWs3.js → chunk-4TB4RGXK-DtM8-CUn.js} +1 -1
  33. package/payload/server/public/assets/{chunk-5FUZZQ4R-iDI6Xu0U.js → chunk-5FUZZQ4R-CrSQ4ySU.js} +1 -1
  34. package/payload/server/public/assets/{chunk-5PVQY5BW-SQD_EpYa.js → chunk-5PVQY5BW-Bn2nQwdj.js} +1 -1
  35. package/payload/server/public/assets/{chunk-EDXVE4YY-CtCcA7_e.js → chunk-EDXVE4YY-CzCPnR0P.js} +1 -1
  36. package/payload/server/public/assets/{chunk-ENJZ2VHE-pXVGVCbb.js → chunk-ENJZ2VHE-CgZj9RoG.js} +1 -1
  37. package/payload/server/public/assets/{chunk-ICPOFSXX-Dkzg9o2N.js → chunk-ICPOFSXX-wy-eNjwW.js} +1 -1
  38. package/payload/server/public/assets/{chunk-OYMX7WX6-1ZZWzf9F.js → chunk-OYMX7WX6-CMmJtL8S.js} +1 -1
  39. package/payload/server/public/assets/{chunk-U2HBQHQK-CpQ3kzO0.js → chunk-U2HBQHQK-CFCW7OaT.js} +1 -1
  40. package/payload/server/public/assets/{chunk-X2U36JSP-C2LkxroC.js → chunk-X2U36JSP-Bgh-CJSN.js} +1 -1
  41. package/payload/server/public/assets/{chunk-YZCP3GAM-Bl5jBOt5.js → chunk-YZCP3GAM-BXKwZ4vN.js} +1 -1
  42. package/payload/server/public/assets/{chunk-ZZ45TVLE-CHtnptPS.js → chunk-ZZ45TVLE-BiOuK5NP.js} +1 -1
  43. package/payload/server/public/assets/classDiagram-6PBFFD2Q-C3IDJsqN.js +1 -0
  44. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BOIrJ5Zb.js +1 -0
  45. package/payload/server/public/assets/clone-P8Fkz7JD.js +1 -0
  46. package/payload/server/public/assets/{dagre-BziN0Nkh.js → dagre-DyCxa9Q2.js} +1 -1
  47. package/payload/server/public/assets/{dagre-KV5264BT-B7OG1g1Y.js → dagre-KV5264BT-Cffo_hmf.js} +1 -1
  48. package/payload/server/public/assets/data-DRfwJPja.js +1 -0
  49. package/payload/server/public/assets/{diagram-5BDNPKRD-Bf31nIDs.js → diagram-5BDNPKRD-Bt93Du3V.js} +1 -1
  50. package/payload/server/public/assets/{diagram-G4DWMVQ6-DQu85hhH.js → diagram-G4DWMVQ6-v4r-tBsC.js} +1 -1
  51. package/payload/server/public/assets/{diagram-MMDJMWI5-5tstbs4y.js → diagram-MMDJMWI5-DKrVPOP-.js} +1 -1
  52. package/payload/server/public/assets/{diagram-TYMM5635--MOV1U4o.js → diagram-TYMM5635-B1PzWQb9.js} +1 -1
  53. package/payload/server/public/assets/{erDiagram-SMLLAGMA-BtAnOJmd.js → erDiagram-SMLLAGMA-ClQzsQAs.js} +1 -1
  54. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CybiCIih.js → flowDiagram-DWJPFMVM-h3IbpN_7.js} +1 -1
  55. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-B44jN7Cy.js → ganttDiagram-T4ZO3ILL-ClaxOJi8.js} +1 -1
  56. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CT7iYcwg.js → gitGraphDiagram-UUTBAWPF-pM-U4ClD.js} +1 -1
  57. package/payload/server/public/assets/graph-B4FFYwus.js +1 -0
  58. package/payload/server/public/assets/graph-labels-Cpk9Ktt0.js +1 -0
  59. package/payload/server/public/assets/{graphlib-DP4o0pYL.js → graphlib-CwimOv_M.js} +1 -1
  60. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CvxFpFHN.js → infoDiagram-42DDH7IO-DcO3Giwe.js} +1 -1
  61. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CUwWLPeR.js → ishikawaDiagram-UXIWVN3A-BcdSL5VS.js} +1 -1
  62. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-y-OnGgLi.js → journeyDiagram-VCZTEJTY-CYahHYzf.js} +1 -1
  63. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BhrSe8R4.js → kanban-definition-6JOO6SKY-Cl5KhoS4.js} +1 -1
  64. package/payload/server/public/assets/lib--yuBd0Xi.js +33 -0
  65. package/payload/server/public/assets/{line-C9uS7z4J.js → line-d_2oTxLp.js} +1 -1
  66. package/payload/server/public/assets/{mermaid-parser.core-Cg4ZdKp-.js → mermaid-parser.core-CMMDGv3x.js} +1 -1
  67. package/payload/server/public/assets/{mermaid.core-BHdKOsex.js → mermaid.core-DK9ENGbr.js} +3 -3
  68. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-NBYiXHo7.js → mindmap-definition-QFDTVHPH-mA3x3MFG.js} +1 -1
  69. package/payload/server/public/assets/page-CgDmg_fX.js +1 -0
  70. package/payload/server/public/assets/{page-D9YpwhIu.js → page-D9lluVl7.js} +2 -2
  71. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CbojC64C.js → pieDiagram-DEJITSTG-CW1nGFlY.js} +1 -1
  72. package/payload/server/public/assets/public-DdLMkNdS.js +7 -0
  73. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CnnoZXcL.js → quadrantDiagram-34T5L4WZ-DI5igJhR.js} +1 -1
  74. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DFRFRtyJ.js → requirementDiagram-MS252O5E-DyVeI4e_.js} +1 -1
  75. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DU5gcpzO.js → sankeyDiagram-XADWPNL6-CCTTBYKY.js} +1 -1
  76. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BN7HZ6Hq.js → sequenceDiagram-FGHM5R23-hVVf35ly.js} +1 -1
  77. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DG6cCjvg.js → stateDiagram-FHFEXIEX-CSLuQDUX.js} +1 -1
  78. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-C5JejhQU.js +1 -0
  79. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-qmV-8SEV.js → timeline-definition-GMOUNBTQ-sxC3W8Q4.js} +1 -1
  80. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BMXQ1s3n.js → vennDiagram-DHZGUBPP-Ro-ton6Z.js} +1 -1
  81. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CoAdP1Gw.js → wardleyDiagram-NUSXRM2D-T-dKD0Zx.js} +1 -1
  82. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CZlfGTEn.js → xychartDiagram-5P7HB3ND-CfProPts.js} +1 -1
  83. package/payload/server/public/data.html +4 -4
  84. package/payload/server/public/graph.html +5 -5
  85. package/payload/server/public/index.html +7 -7
  86. package/payload/server/public/public.html +4 -4
  87. package/payload/server/server.js +299 -1392
  88. package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +0 -154
  89. package/payload/server/chunk-BDFOTLPW.js +0 -759
  90. package/payload/server/chunk-JRBCOVA4.js +0 -1305
  91. package/payload/server/cloudflare-task-tracker-M5ONAGUT.js +0 -22
  92. package/payload/server/public/assets/admin-BvzMvMGo.js +0 -217
  93. package/payload/server/public/assets/channel-DThrH4QF.js +0 -1
  94. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BC6oGTNX.js +0 -1
  95. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-COwC5Umh.js +0 -1
  96. package/payload/server/public/assets/clone-Chp7hvnA.js +0 -1
  97. package/payload/server/public/assets/data-BAgaPs4j.js +0 -1
  98. package/payload/server/public/assets/graph-Cma7EArf.js +0 -1
  99. package/payload/server/public/assets/graph-labels-DyKk6Sxf.js +0 -1
  100. package/payload/server/public/assets/lib-CpkYtEDz.js +0 -29
  101. package/payload/server/public/assets/page-B4IWl3aZ.js +0 -1
  102. package/payload/server/public/assets/public-BtOXjy3A.js +0 -8
  103. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvVd1Q_q.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -31,7 +31,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is dr
31
31
 
32
32
  | Script | Purpose |
33
33
  |---|---|
34
- | [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel resolve (operator-supplied identity), DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Required env: `STREAM_LOG_PATH`, `ACCOUNT_DIR`, AND exactly one of `TUNNEL_ID` (operator selected an existing tunnel — the agent enumerates them via `cloudflared tunnel list --output json` and presents the list in chat) or `TUNNEL_NAME` (operator typed a name to create) per the operator-selected-tunnel fix. The pre-fix derivation `${BRAND}-$(hostname -s)` is removed — the operator's logged-in Cloudflare account is the source of truth for which tunnel exists. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Step 1 (wrappers faithfully relay third-party CLI) spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and prints it as `OAUTH_URL: <url>` on its own stdout — the admin UI's `ActionLogPanel` renders that line as a clickable Authorize link with `target=_blank` so the operator authorizes Cloudflare in a new tab of their own browser. The script does not spawn a browser of its own; cloudflared's OAuth callback writes `~/.cloudflared/cert.pem` regardless of which browser completed the Authorize click. 180 s budget with a 2-second `step=oauth-login result=awaiting-cert` heartbeat. No CDP auto-click, no DOM matcher. |
34
+ | [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel resolve (operator-supplied identity), DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Required env: `ACCOUNT_DIR`, AND exactly one of `TUNNEL_ID` (operator selected an existing tunnel — the agent enumerates them via `cloudflared tunnel list --output json` and presents the list in chat) or `TUNNEL_NAME` (operator typed a name to create). Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Step 1 (wrappers faithfully relay third-party CLI) spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and prints it as `OAUTH_URL: <url>` on its own stdout — the native Claude Code PTY linkifies bare URLs so the operator clicks it from chat and authorizes Cloudflare in their own browser. The script does not spawn a browser of its own; cloudflared's OAuth callback writes `~/.cloudflared/cert.pem` regardless of which browser completed the Authorize click. 180 s budget with a 2-second `step=oauth-login result=awaiting-cert` heartbeat. No CDP auto-click, no DOM matcher. |
35
35
  | [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. No polling blocks — every long-wait is bounded by `cloudflared`'s network round-trip, so no heartbeat contract applies. |
36
36
 
37
37
  ### Skills
@@ -24,17 +24,18 @@
24
24
 
25
25
  set -euo pipefail
26
26
 
27
- # --------------------------------------------------------------------------
28
- # Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, …).
29
- # --------------------------------------------------------------------------
30
-
31
- # shellcheck source=_stream-log.sh
32
27
  # Resolve symlinks before dirname — ~/list-cf-domains.sh is installed as a
33
28
  # symlink into $HOME, so the raw BASH_SOURCE[0] points at $HOME, not the
34
- # scripts directory where _stream-log.sh lives.
29
+ # scripts directory.
35
30
  SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
36
- source "${SCRIPT_DIR}/_stream-log.sh"
37
- require_stream_log_path list-cf-domains
31
+
32
+ # Phase-line helper. Stream-log plumbing retired in Task 287 — every
33
+ # observation goes straight to stdout for the PTY to render.
34
+ phase_line() {
35
+ local scope=$1
36
+ shift
37
+ printf '[%s] %s\n' "${scope}" "$*"
38
+ }
38
39
 
39
40
  if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then
40
41
  phase_line list-cf-domains phase=error reason=brand-arg-missing
@@ -45,10 +46,9 @@ BRAND="${1}"
45
46
  CONFIG_DIR=".${BRAND}"
46
47
  mkdir -p "${HOME}/${CONFIG_DIR}/logs"
47
48
 
48
- # MAXY_PLATFORM_ROOT is set by the systemd service when the admin server
49
- # spawns this script via runFormSpawn. Direct-SSH invocation has no such env
50
- # so derive it from the resolved script location: scripts/ cloudflare/
51
- # plugins/ → platform → install root (three dirs up).
49
+ # MAXY_PLATFORM_ROOT may be set by the calling environment. Direct-from-Bash
50
+ # invocation has no such env so derive it from the resolved script location:
51
+ # scripts/ cloudflare/ plugins/ platform install root (three dirs up).
52
52
  if [ -z "${MAXY_PLATFORM_ROOT:-}" ]; then
53
53
  MAXY_PLATFORM_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
54
54
  fi
@@ -34,15 +34,15 @@
34
34
  // zones at t=1000 ms). The `scrapeDomains` poll waits for the observed count
35
35
  // to stabilise across two consecutive iterations before returning.
36
36
  //
37
- // Contract with the caller (list-cf-domains.sh route → form):
37
+ // Contract with the caller (the agent invokes list-cf-domains.sh via Bash):
38
38
  // stdout on exit 0: JSON `string[]` (empty array means signed-in-but-empty)
39
- // stderr on any path: phase_line-formatted lines, `[list-cf-domains] …`
39
+ // stderr on any path: `[list-cf-domains] …` phase lines
40
40
  // exit 1: `reason=<enum>` on stderr; scrape-empty-but-no-error is exit 0
41
- // with body dump (the enum exists so the caller can distinguish
42
- // "selector drift" from "empty account" — both shapes arrive via
43
- // stdout).
41
+ // with body dump (the enum exists so the operator/agent can
42
+ // distinguish "selector drift" from "empty account" — both
43
+ // shapes arrive via stdout).
44
44
 
45
- import { appendFileSync, readFileSync } from "node:fs";
45
+ import { readFileSync } from "node:fs";
46
46
  import { writeFile } from "node:fs/promises";
47
47
  import { resolve } from "node:path";
48
48
  import { homedir } from "node:os";
@@ -90,45 +90,11 @@ type Reason =
90
90
  | "brand-config-missing"
91
91
  | "cdp-port-unresolved";
92
92
 
93
- // Sticky flag: once a stream-log write fails (EACCES, ENOENT, …), skip
94
- // subsequent file writes for the rest of this process. Repeated appends to an
95
- // unwritable path would spam stderr with identical `phase=stream-log-write-failed`
96
- // lines and crowd result.stderr that the route handler regex reads. Losing a
97
- // log line is strictly better than aborting the scrape — observability failure
98
- // must not mask the scrape's real signal.
99
- let streamLogWriteFailed = false;
100
-
101
93
  function logPhase(line: string): void {
102
- // Stderr: preserved for direct-SSH invocation parity AND for runFormSpawn's
103
- // in-memory `result.stderr`, which the route handler greps for `reason=<enum>`
104
- // on non-zero exit. Removing this write would break the form's error-card path.
94
+ // Stderr: the agent-via-Bash invocation streams the script's stderr into
95
+ // the PTY chat verbatim, so phase lines land in the operator's view
96
+ // alongside any other script output.
105
97
  process.stderr.write(`[list-cf-domains] ${line}\n`);
106
-
107
- // Stream log: direct write by the TS helper (not via wrapper tee) so phase
108
- // lines reach the per-conversation file the tailer reads, even on
109
- // exit 0 where runFormSpawn discards `result.stderr`. Format parity with
110
- // `_stream-log.sh phase_line`: `[<ISO-ts>] [script:<scope>] <content>\n` —
111
- // the `script:` prefix is the chat-surface namespace so the line
112
- // passes the tightened tailer regex (`[mcp:*]` / `[init]` / `[api-wait-*]`
113
- // lines in the same file are excluded by construction). Asserted by the
114
- // vitest regression under platform/ui/__tests__. Sync append so the
115
- // terminal `phase=script-exit code=0 count=N` lands before process exit.
116
- const streamLogPath = process.env.STREAM_LOG_PATH;
117
- if (!streamLogPath || streamLogWriteFailed) return;
118
- try {
119
- appendFileSync(
120
- streamLogPath,
121
- `[${new Date().toISOString()}] [script:list-cf-domains] ${line}\n`,
122
- );
123
- } catch (err) {
124
- streamLogWriteFailed = true;
125
- const detail = (err instanceof Error ? err.message : String(err))
126
- .slice(0, 200)
127
- .replace(/"/g, "'");
128
- process.stderr.write(
129
- `[list-cf-domains] phase=stream-log-write-failed detail="${detail}"\n`,
130
- );
131
- }
132
98
  }
133
99
 
134
100
  function die(reason: Reason, detail = ""): never {
@@ -14,32 +14,32 @@
14
14
  # hostnames. The script writes the ingress rule for them but prints an
15
15
  # explicit ACTION REQUIRED message naming the manual dashboard step.
16
16
  #
17
- # Step 1 surfaces the argotunnel URL on stdout as an `OAUTH_URL:` line.
18
- # The admin UI renders that line as a clickable link (target=_blank) so the
19
- # operator authorizes Cloudflare in the same browser they are already using
20
- # to talk to the admin chat. The script spawns no browser of its own — the
21
- # operator's local browser is the canonical operator-visible surface; the
22
- # brand VNC iframe is derived/optional and never the OAuth-completion path.
23
- # cloudflared's stdout+stderr is teed line-by-line into STREAM_LOG_PATH so
24
- # the chat UI's server-side tailer renders live progress in-turn.
17
+ # Step 1 surfaces the argotunnel URL on stdout as an `OAUTH_URL:` line. The
18
+ # native Claude Code PTY linkifies bare URLs so the operator clicks it from
19
+ # their admin chat directly. The script spawns no browser of its own.
20
+ # cloudflared's stdout+stderr stream verbatim into the PTY chat alongside
21
+ # the script's own `[setup-tunnel] step=<phase> …` phase lines.
25
22
 
26
23
  set -euo pipefail
27
24
 
28
25
  # Resolve symlinks before dirname — ~/setup-tunnel.sh is installed as a symlink
29
26
  # (see packages/create-maxy-code/src/index.ts:installTunnelScripts), so the raw
30
27
  # BASH_SOURCE[0] points at $HOME, not the scripts directory where the sibling
31
- # helpers (_stream-log.sh, tunnel-ingress.ts) live. Factored to SCRIPT_DIR so
32
- # the stream-log-contract scanner can statically resolve the
33
- # tunnel-ingress.ts target.
28
+ # helper (tunnel-ingress.ts) lives.
34
29
  SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
35
30
 
36
31
  # --------------------------------------------------------------------------
37
- # Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, …).
32
+ # Phase-line helper. Stream-log plumbing retired in Task 287 — every
33
+ # observation goes straight to stdout for the PTY to render. Format is
34
+ # `[<scope>] <key>=<value> …` to keep operator-grep parity with the prior
35
+ # `[<scope>] phase=…` shape that the now-deleted _stream-log.sh emitted.
38
36
  # --------------------------------------------------------------------------
39
37
 
40
- # shellcheck source=_stream-log.sh
41
- source "${SCRIPT_DIR}/_stream-log.sh"
42
- require_stream_log_path setup-tunnel
38
+ phase_line() {
39
+ local scope=$1
40
+ shift
41
+ printf '[%s] %s\n' "${scope}" "$*"
42
+ }
43
43
 
44
44
  # --------------------------------------------------------------------------
45
45
  # Args
@@ -123,13 +123,12 @@ fi
123
123
  # cert.pem, mv.
124
124
  #
125
125
  # Control flow when both certs are missing:
126
- # 1. Spawn cloudflared with stdout+stderr teed line-by-line to
127
- # $STREAM_LOG_PATH with prefix [script:setup-tunnel:cloudflared]
128
- # (the chat-surface namespace — see _stream-log.sh header).
126
+ # 1. Spawn cloudflared; its stdout+stderr stream into the PTY via stderr
127
+ # prefixed with `[script:setup-tunnel:cloudflared]`.
129
128
  # 2. Extract the authorize URL with a tolerant regex as it streams.
130
- # 3. Print the URL on stdout as `OAUTH_URL: <url>` so the admin UI
131
- # renders a clickable link. The script does not spawn a browser
132
- # the operator's local browser is the canonical surface; cloudflared's
129
+ # 3. Print the URL on stdout as `OAUTH_URL: <url>` the native Claude
130
+ # Code PTY linkifies it. The script does not spawn a browser; the
131
+ # operator's local browser is the canonical surface. cloudflared's
133
132
  # OAuth callback writes ~/.cloudflared/cert.pem regardless of which
134
133
  # browser completed the Authorize click (it's a server-to-server poll
135
134
  # against login.cloudflareaccess.org).
@@ -192,16 +191,15 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
192
191
  }
193
192
  trap cleanup_oauth EXIT
194
193
 
195
- # cloudflared is line-buffered (stdbuf), teed to the stream log, URL
196
- # extracted as it streams. The subshell holds the whole pipeline so
197
- # PIPESTATUS[0] (cloudflared's exit code) is reachable later.
194
+ # cloudflared is line-buffered (stdbuf); each line is echoed verbatim into
195
+ # the PTY (stderr) while the OAuth URL is also extracted as it streams.
196
+ # The subshell holds the whole pipeline so PIPESTATUS[0] (cloudflared's
197
+ # exit code) is reachable later.
198
198
  (
199
199
  stdbuf -oL -eL cloudflared \
200
200
  --origincert "${CFG_DIR}/cert.pem" tunnel login 2>&1 |
201
201
  while IFS= read -r line; do
202
- ts="$(stream_log_ts)"
203
- printf '[%s] [script:setup-tunnel:cloudflared] %s\n' "${ts}" "${line}" >> "${STREAM_LOG_PATH}"
204
- printf '%s\n' "${line}" >&2
202
+ printf '[script:setup-tunnel:cloudflared] %s\n' "${line}" >&2
205
203
  printf '%s\n' "${line}" > "${LAST_LINE_FILE}"
206
204
  if [ ! -s "${URL_FILE}" ]; then
207
205
  url="$(printf '%s' "${line}" | grep -oE 'https://dash\.cloudflare\.com/argotunnel\?[^ ]+' | head -1 || true)"
@@ -240,8 +238,8 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
240
238
  AUTH_URL="$(cat "${URL_FILE}")"
241
239
  phase_line setup-tunnel step=oauth-url-extracted url_extracted=1
242
240
 
243
- # Emit the URL on stdout in a shape ActionLogPanel's regex captures.
244
- # The admin UI renders this line as a clickable link (target=_blank) so
241
+ # Emit the URL on stdout in a shape the PTY linkifies natively.
242
+ # The native Claude Code terminal renderer turns bare URLs into clicks so
245
243
  # the operator authorizes Cloudflare in the same browser they are using
246
244
  # for the admin chat. The script does not spawn a browser of its own.
247
245
  printf 'OAUTH_URL: %s\n' "${AUTH_URL}"
@@ -22,9 +22,9 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
22
22
 
23
23
  Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit — all in one invocation. 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. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
24
24
 
25
- Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and prints it on its own stdout as `OAUTH_URL: <url>` plus a `step=oauth-url-extracted url_extracted=1` phase line. The admin UI's `ActionLogPanel` renders that `OAUTH_URL` line as a clickable Authorize link with `target=_blank`, opening a new tab in the operator's own browser — the canonical operator-visible OAuth surface. The wrapper does not spawn a browser of its own; cloudflared's OAuth callback polls Cloudflare server-to-server and writes `~/.cloudflared/cert.pem` regardless of which browser completed the Authorize click. The wrapper's cert-poll (180 s budget) picks the cert up and `mv`s it to the brand-scoped path. There is no CDP auto-click, no DOM matcher, no consent-page driver — the wrapper's job is to faithfully relay `cloudflared tunnel login` and surface the URL for the operator to click.
25
+ Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and prints it on its own stdout as `OAUTH_URL: <url>` plus a `step=oauth-url-extracted url_extracted=1` phase line. The native Claude Code PTY linkifies bare URLs, so the `OAUTH_URL: <https…>` line surfaces in chat as a clickable link the operator opens in a new tab of their own browser — the canonical operator-visible OAuth surface. The wrapper does not spawn a browser of its own; cloudflared's OAuth callback polls Cloudflare server-to-server and writes `~/.cloudflared/cert.pem` regardless of which browser completed the Authorize click. The wrapper's cert-poll (180 s budget) picks the cert up and `mv`s it to the brand-scoped path. There is no CDP auto-click, no DOM matcher, no consent-page driver — the wrapper's job is to faithfully relay `cloudflared tunnel login` and surface the URL for the operator to click.
26
26
 
27
- **Operator-visible-surface doctrine.** The operator's visible surface is their local browser — the one already showing the admin UI. The brand VNC iframe is a derived, optional surface that the operator may or may not have open. Any agent claim that ties a UI outcome to a Pi-side surface (a brand VNC chromium tab, a `:N` display) is wrong by construction — the operator may not be watching that surface. Surfaces the agent can trust as operator-visible: the admin chat itself, the admin UI's ActionLogPanel banner, and links the operator clicked from either of those two. Everything else is best-effort.
27
+ **Operator-visible-surface doctrine.** The operator's visible surface is the admin chat PTY itself plus their own local browser. Any agent claim that ties a UI outcome to a Pi-side surface (a brand VNC chromium tab, a `:N` display) is wrong by construction — the operator may not be watching that surface. Surfaces the agent can trust as operator-visible: the admin chat PTY (the script's stdout, including its `OAUTH_URL` line) and pages the operator opened from links rendered in that chat. Everything else is best-effort.
28
28
 
29
29
  ### How inputs reach the script
30
30
 
@@ -32,9 +32,10 @@ The agent collects inputs in plain chat, then invokes the script via Bash. Four
32
32
 
33
33
  Sequence:
34
34
 
35
- 1. **Set the admin password** before invoking the script. Use the same endpoint onboarding uses `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password -H 'Content-Type: application/json' -d '{"password":"…"}'` where `${PORT}` is read from `brand.json`.
35
+ 0. **Derive `BRAND` from disk.** `BRAND=$(jq -r .hostname ~/.<configDir>/brand.json)` never hardcode `maxy` or any literal brand name. The same install can host different brands on different ports; the `brand.json.hostname` field is the only authoritative source. Substituting the wrong literal here cascades into a script writing to the wrong `~/.${BRAND}/` tree and restarting the wrong service.
36
+ 1. **Set the admin password only if not configured.** Gate on `[ -s "${HOME}/.${BRAND}/remote-password" ]` first — when the file is non-empty, a password already exists from a prior onboarding run. Skip the set step entirely. Only when the file is missing or empty, prompt the operator for the password and POST it to `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password -H 'Content-Type: application/json' -d '{"password":"…"}'` (`${PORT}` from `brand.json`). Rotation requires the operator to ask for it explicitly — never re-set on every retry.
36
37
  2. **(Optional) enumerate existing tunnels** when the operator wants to pick from the logged-in account rather than create a new one. Invoke `cloudflared tunnel list --output json` via Bash, present the result as a numbered list, and let the operator type the number (use that tunnel's `id`) or a fresh name.
37
- 3. **Invoke the script** with `~/setup-tunnel.sh <brand> <port> <admin-fqdn> [<public-fqdn>] [<apex-fqdn>]`. Pass either `TUNNEL_ID=<uuid>` (operator selected) or `TUNNEL_NAME=<name>` (operator named a new one) in env — exactly one. `STREAM_LOG_PATH` and `ACCOUNT_DIR` are resolved from `brand.json` and the `${HOME}/.${BRAND}/` layout.
38
+ 3. **Invoke the script** with `~/setup-tunnel.sh "$BRAND" "$PORT" "$ADMIN_FQDN" [<public-fqdn>] [<apex-fqdn>]`. Pass either `TUNNEL_ID=<uuid>` (operator selected) or `TUNNEL_NAME=<name>` (operator named a new one) in env — exactly one. `ACCOUNT_DIR` is resolved from the `${HOME}/.${BRAND}/` layout.
38
39
  4. **Alias-domain classification.** For each non-admin hostname that does not start with `public.`, append it to `~/{configDir}/alias-domains.json` so `isPublicHost()` treats it as public. The platform server watches that file (`watchFile`, ~2 s pickup) — no restart needed. Use this idempotent Bash command per hostname (HOST is the FQDN, FILE is `~/.${BRAND}/alias-domains.json`):
39
40
 
40
41
  ```bash
@@ -52,10 +53,14 @@ Repeated calls with the same hostname are a no-op (the `| unique` filter dedupes
52
53
  ~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]
53
54
  ```
54
55
 
55
- Example ({{productName}} on `maxy.bot` with a public subdomain and the `maxy.chat` apex):
56
+ Examples placeholders show the substitution shape; the literal `BRAND` always derives from `brand.json.hostname`:
56
57
 
57
58
  ```
58
- ~/setup-tunnel.sh maxy 19200 admin.maxy.bot public.maxy.bot maxy.chat
59
+ BRAND=$(jq -r .hostname ~/.maxy/brand.json) # e.g. yields "maxy"
60
+ ~/setup-tunnel.sh "$BRAND" 19200 admin.maxy.bot public.maxy.bot maxy.chat
61
+
62
+ BRAND=$(jq -r .hostname ~/.maxy-code/brand.json) # e.g. yields "maxy-code"
63
+ ~/setup-tunnel.sh "$BRAND" 19200 admin.maxy.bot public.maxy.bot maxy.chat
59
64
  ```
60
65
 
61
66
  ### Optional SSH and SMB ingress
@@ -104,12 +109,12 @@ The agent invokes the script directly via the Bash tool — there is no form, no
104
109
 
105
110
  ### Narrating OAuth progress
106
111
 
107
- The script tees structured phase lines into the stream log (`[setup-tunnel] step=<phase> <key=value …>`). The admin UI renders the `OAUTH_URL` stdout line as a clickable Authorize link directly above the log panel; the operator clicks it and authorizes in a new tab of their own browser. The agent's narration job is to let that link do the work and not claim where any tab is open.
112
+ The script prints structured phase lines to stdout (`[setup-tunnel] step=<phase> <key=value …>`) these stream straight into the PTY chat verbatim. The `OAUTH_URL: <https…>` line is on a line by itself; the native Claude Code terminal renderer linkifies it so the operator clicks the link directly from chat and authorises in their own browser. The agent's narration job is to let that link do the work and not claim where any tab is open.
108
113
 
109
- - **When `step=oauth-url-extracted url_extracted=1` appears,** the URL is already surfaced as a clickable link in the operator's chat. The agent says one short line — "click the Authorize link and I'll pick up the cert once you've authorised" — and waits. No claim about which device or screen the link opens on; the operator's browser handles that.
114
+ - **When `step=oauth-url-extracted url_extracted=1` appears,** the `OAUTH_URL: …` line above it is already clickable in the operator's chat. The agent says one short line — "click the Authorize link above and I'll pick up the cert once you've authorised" — and waits. No claim about which device or screen the link opens on; the operator's browser handles that.
110
115
  - **When `step=oauth-login result=ok reason=cert-promoted-from-default-path` appears,** a prior run already completed OAuth — Step 1 short-circuits to the existing cert. No operator action is needed.
111
116
  - **When `step=oauth-login result=error reason=<token>` appears,** the agent restates the literal `reason=…` and any `last_line=…` field and stops per the discipline rule below. The reasons that can fire on this path are `cloudflared-exited-before-url`, `url-not-extracted`, `timeout-waiting-cert`, `cloudflared-exited-no-cert`, and `cert-promote-failed`. `timeout-waiting-cert` specifically means the operator did not click Authorize within 180 s; the remediation is a fresh `~/setup-tunnel.sh` invocation, which will land on the cert-promotion pre-flight if the operator authorised after the timeout.
112
- - **Stream-log `result=ok` on its own steps is not narration evidence.** The agent narrates from operator-action outcomes — link clicked, cert landed — not from script-internal phase ticks.
117
+ - **`result=ok` on a script-internal phase is not narration evidence.** The agent narrates from operator-action outcomes — link clicked, cert landed — not from internal phase ticks.
113
118
 
114
119
  ### When the script exits non-zero
115
120
 
@@ -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 `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]` via the Bash tool. The script's `[setup-tunnel] step=<phase> …` phase lines and cloudflared's own stdout stream into the PTY; the `OAUTH_URL: <url>` line 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 scripts 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 shell scripts (`setup-tunnel.sh`, `reset-tunnel.sh`, `list-cf-domains.sh`) print `[<scope>] <key>=<value>` phase lines directly to stdout, which the PTY renders in chat verbatim.
156
156
 
157
157
  **Retrieve MCP diagnostic lines for a conversation:**
158
158