@rubytech/create-maxy-code 0.1.219 → 0.1.222

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 +46 -1
  2. package/package.json +1 -1
  3. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +60 -23
  4. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -1
  5. package/payload/platform/lib/graph-write/dist/index.d.ts +4 -1
  6. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  7. package/payload/platform/lib/graph-write/dist/index.js +17 -5
  8. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  9. package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +58 -31
  10. package/payload/platform/lib/graph-write/src/index.ts +16 -7
  11. package/payload/platform/plugins/admin/PLUGIN.md +2 -3
  12. package/payload/platform/plugins/admin/__tests__/admin-can-enumerate.test.sh +129 -0
  13. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +1 -2
  14. package/payload/platform/plugins/admin/hooks/lib/hook-emit.sh +3 -2
  15. package/payload/platform/plugins/admin/mcp/dist/index.js +41 -9
  16. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/admin/references/publish-site-routing.md +44 -0
  18. package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +1 -1
  19. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +11 -4
  20. package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +2 -2
  21. package/payload/platform/plugins/contacts/PLUGIN.md +2 -2
  22. package/payload/platform/plugins/docs/references/internals.md +1 -1
  23. package/payload/platform/plugins/docs/references/platform.md +2 -2
  24. package/payload/platform/plugins/docs/references/troubleshooting.md +7 -0
  25. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  26. package/payload/platform/plugins/memory/references/graph-primitives.md +2 -2
  27. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts +2 -1
  28. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts.map +1 -1
  29. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js +27 -21
  30. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js.map +1 -1
  31. package/payload/platform/scripts/check-no-esm-require.mjs +3 -0
  32. package/payload/platform/scripts/setup-account.sh +0 -15
  33. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.d.ts +4 -0
  34. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.d.ts.map +1 -0
  35. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.js +50 -0
  36. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.js.map +1 -0
  37. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/http-server.js +19 -2
  39. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +39 -19
  42. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +24 -0
  44. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +100 -11
  46. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  47. package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts +8 -0
  48. package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts.map +1 -1
  49. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +7 -2
  50. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -1
  51. package/payload/server/public/assets/{AdminShell-BcxEZnpu.js → AdminShell-BiEwDSRL.js} +1 -1
  52. package/payload/server/public/assets/{Checkbox-DL-HdT29.js → Checkbox-COiFXiXL.js} +1 -1
  53. package/payload/server/public/assets/{admin-DMo5DWKH.js → admin-Ca9ckLZx.js} +1 -1
  54. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BoUw4oYz.js → architectureDiagram-Q4EWVU46-DI6JZI13.js} +1 -1
  55. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DOscnO3m.js → blockDiagram-DXYQGD6D-dgDvswLd.js} +1 -1
  56. package/payload/server/public/assets/{brand-SZW-27T7.css → brand-CAMMU5lz.css} +1 -1
  57. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-aZ1IuZ_F.js → c4Diagram-AHTNJAMY-odaGY4GZ.js} +1 -1
  58. package/payload/server/public/assets/channel-DfwLbKck.js +1 -0
  59. package/payload/server/public/assets/{chunk-336JU56O-DS3ksXBG.js → chunk-336JU56O-BpbY8F27.js} +2 -2
  60. package/payload/server/public/assets/{chunk-426QAEUC-BLRn2STO.js → chunk-426QAEUC-BWbVhEVC.js} +1 -1
  61. package/payload/server/public/assets/{chunk-4TB4RGXK-BrhOvJ01.js → chunk-4TB4RGXK-C3IfRdvA.js} +1 -1
  62. package/payload/server/public/assets/{chunk-5FUZZQ4R-BZZwZX5Q.js → chunk-5FUZZQ4R-CSRMBAGr.js} +1 -1
  63. package/payload/server/public/assets/{chunk-5PVQY5BW-YXZKnPCz.js → chunk-5PVQY5BW-CGAe6GJJ.js} +1 -1
  64. package/payload/server/public/assets/{chunk-EDXVE4YY-DAMCJHKQ.js → chunk-EDXVE4YY-BnXC8cYQ.js} +1 -1
  65. package/payload/server/public/assets/{chunk-ENJZ2VHE-DsD95Bqn.js → chunk-ENJZ2VHE-7qfDalAB.js} +1 -1
  66. package/payload/server/public/assets/{chunk-ICPOFSXX-DxtrRClx.js → chunk-ICPOFSXX-TRvVGIDw.js} +1 -1
  67. package/payload/server/public/assets/{chunk-OYMX7WX6-CN4kLK-H.js → chunk-OYMX7WX6-Cfv8vBvI.js} +1 -1
  68. package/payload/server/public/assets/{chunk-U2HBQHQK-BPTv3Oj1.js → chunk-U2HBQHQK-BfLvbEyu.js} +1 -1
  69. package/payload/server/public/assets/{chunk-X2U36JSP-DxBry33v.js → chunk-X2U36JSP-Cj3k-dYC.js} +1 -1
  70. package/payload/server/public/assets/{chunk-YZCP3GAM-BZ4N75oy.js → chunk-YZCP3GAM-CygW6I0h.js} +1 -1
  71. package/payload/server/public/assets/{chunk-ZZ45TVLE-Dqr161Q1.js → chunk-ZZ45TVLE-CBF3hFJH.js} +1 -1
  72. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CE0XZQre.js +1 -0
  73. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Bj41Y3XD.js +1 -0
  74. package/payload/server/public/assets/clone-DTd5nONj.js +1 -0
  75. package/payload/server/public/assets/{dagre-DtZ0wKkH.js → dagre-BTGABQJX.js} +1 -1
  76. package/payload/server/public/assets/{dagre-KV5264BT-BPKhT-ZX.js → dagre-KV5264BT-B2lbkuGd.js} +1 -1
  77. package/payload/server/public/assets/{data-D6IQdm0R.js → data-CHdaTjgn.js} +1 -1
  78. package/payload/server/public/assets/{diagram-5BDNPKRD-CATnafi7.js → diagram-5BDNPKRD-BUK4LqT8.js} +1 -1
  79. package/payload/server/public/assets/{diagram-G4DWMVQ6-ChZYQfji.js → diagram-G4DWMVQ6-e49HrvxL.js} +1 -1
  80. package/payload/server/public/assets/{diagram-MMDJMWI5-DHW6kYd3.js → diagram-MMDJMWI5-k4Kx_qWe.js} +1 -1
  81. package/payload/server/public/assets/{diagram-TYMM5635-CnS4Zci8.js → diagram-TYMM5635-DzavOUdK.js} +1 -1
  82. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DKYv2YWp.js → erDiagram-SMLLAGMA-CJKecRhi.js} +1 -1
  83. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BWvoZWlh.js → flowDiagram-DWJPFMVM-GM7H1TDt.js} +1 -1
  84. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-CpCb-xKy.js → ganttDiagram-T4ZO3ILL-kDEA1A-3.js} +1 -1
  85. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CvHrDKQ-.js → gitGraphDiagram-UUTBAWPF-jVZZdeg2.js} +1 -1
  86. package/payload/server/public/assets/{graph-labels-CWJy0O4w.js → graph-labels-CavkO4DG.js} +1 -1
  87. package/payload/server/public/assets/{graph-PkZKVkWD.js → graph-tqCqSngu.js} +1 -1
  88. package/payload/server/public/assets/{graphlib-DjnyHqGy.js → graphlib-DFU2rJiY.js} +1 -1
  89. package/payload/server/public/assets/{infoDiagram-42DDH7IO-D_MywcVn.js → infoDiagram-42DDH7IO-CFxVoRBy.js} +1 -1
  90. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-N9m-f-Eb.js → ishikawaDiagram-UXIWVN3A-CDvZs3Ua.js} +1 -1
  91. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DE0q7uSM.js → journeyDiagram-VCZTEJTY-DUI1m9vW.js} +1 -1
  92. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-Dj5hb9Bq.js → kanban-definition-6JOO6SKY-C_jSVK7W.js} +1 -1
  93. package/payload/server/public/assets/{line-C6ph0i4M.js → line-DfwJeo6J.js} +1 -1
  94. package/payload/server/public/assets/{mermaid-parser.core-BrfmEvCA.js → mermaid-parser.core-BN-2kBfU.js} +1 -1
  95. package/payload/server/public/assets/{mermaid.core-DYnHKfDv.js → mermaid.core-C6amhHAQ.js} +3 -3
  96. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DaSaZfhR.js → mindmap-definition-QFDTVHPH-eTKDlw3c.js} +1 -1
  97. package/payload/server/public/assets/{pieDiagram-DEJITSTG-BAu9Ezbv.js → pieDiagram-DEJITSTG-DNX5VeMZ.js} +1 -1
  98. package/payload/server/public/assets/{public-UKgkJxKv.js → public-BADBLzOZ.js} +3 -3
  99. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DRsbL1Xd.js → quadrantDiagram-34T5L4WZ-BFnmYiHX.js} +1 -1
  100. package/payload/server/public/assets/{requirementDiagram-MS252O5E-B9QcsXV6.js → requirementDiagram-MS252O5E-DvHvTyzu.js} +1 -1
  101. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DsZbvIlD.js → sankeyDiagram-XADWPNL6-CMFnNT1G.js} +1 -1
  102. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BlT4O2N8.js → sequenceDiagram-FGHM5R23-BYon1ouU.js} +1 -1
  103. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-JFgk1K3Y.js → stateDiagram-FHFEXIEX-n6l3hcuJ.js} +1 -1
  104. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CS46-Mhi.js +1 -0
  105. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DOsHw1ip.js → timeline-definition-GMOUNBTQ-DFPFHrQF.js} +1 -1
  106. package/payload/server/public/assets/{useSelectionMode-JDGvwvIk.js → useSelectionMode-iqIyL24g.js} +1 -1
  107. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CQWIhqZY.js → vennDiagram-DHZGUBPP-DZLturYa.js} +1 -1
  108. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-D5Fh7YVL.js → wardleyDiagram-NUSXRM2D-BBt2pdjM.js} +1 -1
  109. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-C-tINQEs.js → xychartDiagram-5P7HB3ND-DOZxLvEi.js} +1 -1
  110. package/payload/server/public/data.html +5 -5
  111. package/payload/server/public/graph.html +6 -6
  112. package/payload/server/public/index.html +6 -6
  113. package/payload/server/public/public.html +5 -5
  114. package/payload/server/server.js +49 -6
  115. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-admin-tool-gate.test.sh +0 -109
  116. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-base64-guard.test.sh +0 -204
  117. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-passthrough.test.sh +0 -118
  118. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +0 -337
  119. package/payload/server/public/assets/channel-CzcSNxDm.js +0 -1
  120. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DFOM3vCn.js +0 -1
  121. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-HplDfaL1.js +0 -1
  122. package/payload/server/public/assets/clone-G-2rqnSR.js +0 -1
  123. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-NXz27DaV.js +0 -1
  124. /package/payload/server/public/assets/{brand-SzZmKKjz.js → brand-DfykKss4.js} +0 -0
@@ -0,0 +1,44 @@
1
+ # Publish-site routing
2
+
3
+ How a published static site goes from disk to a public URL, so the admin agent can find a site or share a link without guessing.
4
+
5
+ ## On-disk layout
6
+
7
+ Every published site lives under the active account:
8
+
9
+ ```
10
+ <accountDir>/sites/<slug>/
11
+ index.html
12
+ …assets
13
+ ```
14
+
15
+ Where `<accountDir>` resolves to `<installDir>/data/accounts/<accountId>/` and `<slug>` is one or more `/`-separated segments accepted by the `publish-site` tool. To enumerate every published site on the account, list `<accountDir>/sites/` — `LS`, `Glob`, or `Bash ls` all work, pick whichever is in the surface.
16
+
17
+ ## URL composition
18
+
19
+ The full published URL is exactly:
20
+
21
+ ```
22
+ https://<public-hostname>/<slug>
23
+ ```
24
+
25
+ `<public-hostname>` is whatever the `public-hostname` tool returns for this account. That tool reads cloudflared's own ingress config and `alias-domains.json` — the same files the platform server trusts to route — so its answer is deterministic. **Do not guess the hostname**: the public landing host for the product family (for example, `realagent.chat`) is not the publish-site host. Each account's sites are served from the account's own admin tunnel, and the canonical name comes from `public-hostname`.
26
+
27
+ ## Recipe — find a site
28
+
29
+ 1. `LS <accountDir>/sites/` (or `Glob '<accountDir>/sites/*'`).
30
+ 2. Pick the slug whose name matches the request.
31
+ 3. Call `public-hostname` once.
32
+ 4. The URL is `https://<hostname>/<slug>`.
33
+
34
+ ## Recipe — share a published site
35
+
36
+ 1. Confirm the slug exists with `LS <accountDir>/sites/<slug>/` (must contain at least an `index.html`).
37
+ 2. Call `public-hostname`.
38
+ 3. Paste `https://<hostname>/<slug>` to the operator.
39
+
40
+ ## Failure modes the agent should not chain
41
+
42
+ - **`browser-render https://<product-host>/<slug>` returning the generic shell.** That host is the marketing landing, not the publish-site host. Resolve the hostname with `public-hostname` instead of typing a guess.
43
+ - **Treating `url-get` `textLength: 0` as "page does not exist".** On a JS-rendered shell the verbatim HTML is fetched but contains no extracted text. The tool returns `ok: true` with `textLength: 0`; use `browser-render` to get the rendered DOM.
44
+ - **Reading `<accountDir>/sites/<slug>/` directly with `Read`.** `Read` is for files, not directories. Use `LS` or `Glob` for directory contents.
@@ -28,7 +28,7 @@ Admin identity lives in three places that must stay in lockstep:
28
28
 
29
29
  ## Never write `account.json` directly
30
30
 
31
- All account-level mutations (`tier`, `outputStyle`, `thinkingView`, `effort`, `enabledPlugins`, `admins[]`, agent settings) go through the dedicated MCP tools: `account-update`, `plugin-toggle-enabled`, `admin-add`, `admin-remove`. The pre-tool-use hook denies direct `Edit` or `Write` on `account.json` server-side; this rule is the explanation for the denial. `tier` and `purchasedPlugins` derive from a Rubytech-signed entitlement payload on commercial installs, so a hand-edit is silently void.
31
+ All account-level mutations (`tier`, `outputStyle`, `thinkingView`, `effort`, `enabledPlugins`, `admins[]`, agent settings) go through the dedicated MCP tools: `account-update`, `plugin-toggle-enabled`, `admin-add`, `admin-remove`. Direct `Edit` or `Write` on `account.json` is forbidden by IDENTITY.md doctrine — there is no server-side gate, so the doctrine line is the only thing standing between an agent slip and a silently corrupted account file. `tier` and `purchasedPlugins` derive from a Rubytech-signed entitlement payload on commercial installs, so a hand-edit is silently void.
32
32
 
33
33
  `tier` is the one account-level field with no admin-agent surface — `account-update` excludes it deliberately, because tier is privilege-bearing and the agent must not self-elevate. When refusing a tier-cap violation (`admin-add` against a full slot, etc.), point the owner at the founder-side path: `npx @rubytech/create-<brand>-code@latest --tier <solo|family|pro>` run on the device by the founder. On commercial installs the founder delivers a signed payload instead: `--entitlement-base64 <base64>`.
34
34
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:d8c18dc6630fbbddd4408fa971e7786cddfe36288714ac1e3840530d18903e91
4
+ content-hash: sha256:cabadf29cd1f98c47b4f93c23339ee577ce7bb41092d085d01268b947165a1d5
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -220,7 +220,7 @@ Each session row also carries a small muted timestamp crumb under the name showi
220
220
 
221
221
  **Two spawn surfaces, one primitive (Task 573).** The manager runs two on-device spawn surfaces, both backed by the same primitive: **node-pty wrapped in `systemd-run --user --scope`** (via `index.ts::spawnPtyAdapter`).
222
222
 
223
- - **`claude rc` daemon** — spawned at platform boot by `rc-daemon.ts`. One supervised daemon per account; owns the long-lived composer session that backs claude.ai/code Remote Control. Master fd held for the daemon's lifetime, released on natural exit / restart.
223
+ - **`claude rc` daemon** — spawned at platform boot by `rc-daemon.ts`. One supervised daemon per account; owns the long-lived composer session that backs claude.ai/code Remote Control. Master fd held for the daemon's lifetime, released on natural exit / restart. **Headless consent pre-seed (Task 578).** Before the first spawn, `ensureRemoteControlConsent` writes `{"remoteControlAtStartup": true}` into `$CLAUDE_CONFIG_DIR/.claude.json` (read-merge-write, atomic tmp+rename, idempotent). Without this, headless `claude rc` hangs at `Enable Remote Control? (y/n)` — nothing answers, the supervisor restarts the child, eventually marks the daemon permanently-failed. The key is the same one `claude` itself writes when the user answers `y` at the prompt; siblings (`teammateMode`, `hasUsedRemoteControl`, claude's auth blocks) are preserved.
224
224
  - **`claude --remote-control` on-device sidebar spawn** — spawned per-click by `/rc-spawn` in `http-server.ts`. One PTY per click; the manager holds the master fd **for the session's entire lifetime**. The pty master IS the live session — claude operates on the slave, and closing the master hangs up the slave. Valid master-release points: (1) explicit operator teardown — `/stop` → `stopSession` → `op=archive-release` — and (2) the natural-exit path inside `pty.onExit → handlePtyNaturalExit`.
225
225
 
226
226
  Inside the scope, `sh -c 'trap "" HUP; exec "$@"' sh <claudeBin> <args...>` keeps claude resident across PTY master-close (SIGHUP trap) and preserves the pid through the exec chain. The earlier `script(1)` wrap and the non-PTY scope primitive (Tasks 552/556/562) are gone; node-pty allocates the TTY directly.
@@ -244,7 +244,7 @@ Inside the scope, `sh -c 'trap "" HUP; exec "$@"' sh <claudeBin> <args...>` keep
244
244
 
245
245
  `verified=true` requires `master-fd=closed` AND `fdDelta>=1` AND `trackerRemoved=true`. `master-fd=close-failed` is logged at error level (`[rc-spawn-error]` prefix) — never swallowed; the next post-archive sweep is the catch-net.
246
246
 
247
- **Cross-arm `[rc-life]` schema.** rc-spawn and rc-daemon emit a shared log shape so the populations can be compared from `server.log` alone. One spawn's full lifeline is `grep <unitToken>`; one surface's signature is `grep 'source=rc-spawn'` or `'source=rc-daemon'`. Success on both surfaces is `op=pidfile-present`; failure is `op=spawn-failed` / `op=early-exit` / `op=wait-pid-failed`. Full schema and operator runbook in [`.docs/rc-life-observability.md`](../../../.docs/rc-life-observability.md).
247
+ **Cross-arm `[rc-life]` schema.** rc-spawn and rc-daemon emit a shared log shape so the populations can be compared from `server.log` alone. One spawn's full lifeline is `grep <unitToken>`; one surface's signature is `grep 'source=rc-spawn'` or `'source=rc-daemon'`. Success on both surfaces is `op=pidfile-present`; failure is `op=spawn-failed` / `op=early-exit` / `op=wait-pid-failed`. Full schema and operator runbook in [`.docs/rc-life-observability.md`](../../../.docs/rc-life-observability.md). **Measured `remoteBound` (Task 578).** The rc-daemon liveness emit reports `remoteBound` as a measured value flipped by `detectRcHandshake` once the daemon's own post-bind output (`Capacity:` header or an `N of M` capacity line) is seen on the PTY. A daemon that is alive but not registered to Remote Control therefore prints `pidAlive=true remoteBound=false` — previously masked by a hardcoded literal. A class-guard test (`rc-life-literals.test.ts`) scans every `emitRcLife` call across the manager and fails if any status-shaped field is set to a boolean/string literal. The captured PTY output is now dumped on **every** exit (not only fast exits), prefix `exit-output` or `fast-exit-output`, so a late-life prompt-hang is no longer invisible.
248
248
 
249
249
  **Post-archive fd sweep (Task 558).** Independent of spawn/archive request traffic, the manager runs a 60 s sweep that walks both directions of the master-fd invariant:
250
250
 
@@ -3134,7 +3134,7 @@ Each log entry includes the tool name and a truncated conversation ID for correl
3134
3134
 
3135
3135
  Every durable action — cloudflare tunnel-login, brand publish, future deterministic flows — emits a `:Task {kind:"<flow>"}` node carrying the action's lifecycle and a `:PRODUCED` edge to every entity the action created. This makes the graph traversable from the originating Conversation to every entity created during it via `(c)<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e)` — answering "what did this turn produce" in one Cypher hop.
3136
3136
 
3137
- The doctrine is enforced at the storage primitive: writes to `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` MUST include at least one inbound `:PRODUCED` edge whose source is one of `:Task`, `:Conversation`, or `:Message`. Subtype labels like `:AdminConversation`, `:UserMessage`, `:AssistantMessage`, `:AdminMessage` qualify because the gate checks the full `labels()` array. Bootstrap writes (PIN-setup, schema migrations, lazy first-session UserProfile creation) are exempt via `createdBy.agent === 'system'`.
3137
+ The doctrine is observed at the storage primitive: writes to `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` should carry an inbound `:PRODUCED` edge whose source is one of `:Task`, `:Conversation`, or `:Message`. Subtype labels like `:AdminConversation`, `:UserMessage`, `:AssistantMessage`, `:AdminMessage` qualify because the gate checks the full `labels()` array. Bootstrap writes (PIN-setup, schema migrations, lazy first-session UserProfile creation) are exempt via `createdBy.agent === 'system'`. When no qualifying edge resolves, the primitive emits a `[graph-write] warn reason=missing-provenance labels=<csv> agent=<agentLabel>` line and the write proceeds (Task 580 relaxed this from a hard reject — the composer-spawned admin path inherits a bare per-account env that never receives the `SESSION_NODE_ID` stamp, so the throw was failing every direct admin contact-create / memory-write for a gated label).
3138
3138
 
3139
3139
  Two surfaces emit the lifecycle: agent-driven actions call `work-create`/`work-update`/`work-complete` over MCP (`work-create` accepts `kind`, the canonical `inputsProvided` call-shape record, `inputs` + `inputSchema` for the operator-meaningful form payload, and `raisedDuringConversationKey` to resolve the `RAISED_DURING` edge). Shell-driven actions wrap their script invocation in [platform/ui/app/lib/cloudflare-task-tracker.ts](../../../ui/app/lib/cloudflare-task-tracker.ts) (cloudflare is the first; installer / brand-publish / OAuth-login deferred). Both surfaces emit the same `[task] action-start|step|done` log lines so operators can grep one channel uniformly. Both also call the central `redactSecrets` primitive ([platform/lib/task-secrets/](../../../lib/task-secrets/)) to strip schema-tagged secret keys before persisting `inputs.<field>` props on the Task — see `.docs/neo4j.md § Audit Task input contract` for the contract that replaces per-kind allow-lists.
3140
3140
 
@@ -3630,6 +3630,13 @@ tail -200 ~/.maxy/logs/maxy-ui.log | rg '\[remote-auth\].*resolvedKind='
3630
3630
  2. `diff <(jq .claudeAiOauth.accessToken ~/.maxy/.claude/.credentials.json) <(jq .claudeAiOauth.accessToken ~/.realagent/.claude/.credentials.json)` — must be non-empty after each brand's operator has run `claude /login` against distinct Anthropic accounts; if it's empty, both brands are still logged in to the same account (operator action, not a code bug).
3631
3631
  3. `grep "\[install\] claude-creds pickup" ~/.${brand}/logs/install-*.log` — fires once on the first post-Task-923 install of any brand and moves the legacy `~/.claude/.credentials.json` into that brand's path. Subsequent brands install with no credentials and require a fresh `claude /login` inside that brand's chat (which writes to the brand-scoped path because the systemd unit env is in scope).
3632
3632
 
3633
+ **All sessions on the brand stopped responding after a token expiry.** Symptom on the operator side: every spawn dies at `pid-file-timeout` and the dashboard health probe reports auth dead. Diagnose the OAuth refresh path before anything else:
3634
+
3635
+ 1. `tail -n 300 ~/.${brand}/logs/server.log | grep -E 'auth-refresh|auth-health|invalid_grant'` — `op=lock-acquired` proves the cross-process lock is in play (Task 576). `op=skipped-fresh` means a sibling process (the admin server or a `claude` binary) already rotated the tokens during the lock wait — expected, healthy. `op=renewed expiresAt=…` is the only line that means a network refresh actually ran.
3636
+ 2. `outcome=fail-token` or `invalid_grant` lines mean Anthropic rejected the refresh token itself (revoked or expired beyond the rotation window). The brand needs a fresh `claude /login`. Pre-576 the most common cause was the admin server and a spawned `claude` racing to rotate the same single-use refresh token; that race is now serialised by the file lock at `~/.${brand}/.claude/.credentials.json.lock` and a re-read after the lock skips redundant refreshes.
3637
+ 3. `grep '\[auth-health\]' ~/.${brand}/logs/server.log | tail -n 5` — the heartbeat fires every five minutes. `status=dead expiresIn=...` means the refresh token is gone; only a re-login fixes it. `status=ok` heartbeats with no spawns in between mean the credentials file is healthy and the failure lives elsewhere.
3638
+ 4. The spawn-failure surface now carries `reason=auth-refresh-failed` (with `authStatus` in the JSON body) instead of generic `pid-file-timeout` whenever the credentials file is in `dead` or `expired` state at the moment of failure — visible in `grep '\[spawn-failed\]'` on server.log.
3639
+
3633
3640
  ---
3634
3641
 
3635
3642
  ## Memory Not Working
@@ -34,7 +34,7 @@ Built-in plugins under `$PLATFORM_ROOT/plugins/`:
34
34
 
35
35
  - Enable: call `plugin-toggle-enabled` with `pluginName` and `action: "enable"`. Activates the plugin's behaviour embed and MCP server from the next session. Plugins with scheduled behaviour (declared via `lifecycle` in PLUGIN.md frontmatter) are activated automatically by the platform heartbeat within one minute — no separate setup command needed.
36
36
  - Disable: call `plugin-toggle-enabled` with `pluginName` and `action: "disable"`. Deactivates from the next session. Any scheduled Events owned by the plugin (`sourcePlugin` field) are cancelled automatically by the platform heartbeat.
37
- - The tool refuses core plugins (admin, memory, docs, cloudflare) and refuses plugins not installed under `$PLATFORM_ROOT/plugins/`. Direct `Edit` of `account.json` is denied by the pre-tool-use hook — `plugin-toggle-enabled` is the only legitimate path.
37
+ - The tool refuses core plugins (admin, memory, docs, cloudflare) and refuses plugins not installed under `$PLATFORM_ROOT/plugins/`. Direct `Edit` of `account.json` is forbidden by IDENTITY.md doctrine — `plugin-toggle-enabled` is the only legitimate path.
38
38
 
39
39
  ## Premium Plugins
40
40
 
@@ -50,7 +50,7 @@ When the user asks about a specific premium plugin (e.g., "tell me about the tea
50
50
 
51
51
  ### Recording a purchase
52
52
 
53
- `purchasedPlugins` is an entitlement-bearing field: the effective list derives from the Rubytech-signed entitlement payload, not from raw `account.json`. The pre-tool-use hook denies any agent write to `account.json`. Direct purchase recording is therefore unavailable to the agent.
53
+ `purchasedPlugins` is an entitlement-bearing field: the effective list derives from the Rubytech-signed entitlement payload, not from raw `account.json`. IDENTITY.md doctrine forbids any agent write to `account.json`. Direct purchase recording is therefore unavailable to the agent.
54
54
 
55
55
  - **Production path:** the Rubytech-side issuance endpoint signs an updated entitlement payload after a Stripe purchase event and delivers it to `~/<configDir>/entitlement.json`. The verifier picks up the new payload at the next session start.
56
56
  - **Pre-launch / dev path:** the founder runs `node platform/scripts/generate-entitlement-fixture.mjs sign --account-id <id> --email <e> --tier <t> --plugins a,b,c --out ~/<configDir>/entitlement.json` to produce a signed test payload. The agent does not invoke this script.
@@ -55,8 +55,8 @@ Tools are available via the `contacts` MCP server.
55
55
 
56
56
  ## Provenance anchoring
57
57
 
58
- `contact-create` writes a `:Person` node, which is one of the action-provenance-gated labels in `writeNodeWithEdges`. The write must carry an inbound `:PRODUCED` edge from `:Task`, `:Conversation`, or `:Message`. There is no agent-visible field for this — the admin server stamps `SESSION_NODE_ID` (the `sessionId` UUID of the active `:AdminConversation`) at MCP spawn time, and the wrapper auto-injects the `:PRODUCED` edge via `injectConversationProvenance`. The helper MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is enforced inside the natural key, not as a separate gate, so a cross-account env-stamp returns zero rows and never injects.
58
+ `contact-create` writes a `:Person` node, which is one of the action-provenance-gated labels in `writeNodeWithEdges`. The write should carry an inbound `:PRODUCED` edge from `:Task`, `:Conversation`, or `:Message`. There is no agent-visible field for this — the admin server stamps `SESSION_NODE_ID` (the `sessionId` UUID of the active `:AdminConversation`) at MCP spawn time, and the wrapper auto-injects the `:PRODUCED` edge via `injectConversationProvenance`. The helper MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is enforced inside the natural key, not as a separate gate, so a cross-account env-stamp returns zero rows and never injects.
59
59
 
60
- The agent passes whichever organic relationships are appropriate (`PARTICIPATES_IN` to a Conversation, `WORKS_FOR` to an Organization, `KNOWN_VIA` to a referral source) — the provenance edge is composed automatically and is not an agent concern. When the env-stamp resolution fails (loud `[mcp:contacts] [provenance-missing] reason=<...>`), the downstream gate emits `missing-provenance` and the write fails closed.
60
+ The agent passes whichever organic relationships are appropriate (`PARTICIPATES_IN` to a Conversation, `WORKS_FOR` to an Organization, `KNOWN_VIA` to a referral source) — the provenance edge is composed automatically and is not an agent concern. When the env-stamp resolution fails (loud `[mcp:contacts] [provenance-missing] reason=<...>`), the downstream gate emits a `[graph-write] warn reason=missing-provenance labels=<csv> agent=<agentLabel>` line and the write proceeds (Task 580 relaxed this from a hard reject). The warn carries the labels and agent so operators can quantify how often the gap fires; the upstream `[provenance-missing]` line names the resolution failure that caused it.
61
61
 
62
62
  See [.docs/neo4j.md § Process provenance doctrine](../../../.docs/neo4j.md) for the doctrine and observability surface.
@@ -502,7 +502,7 @@ Each log entry includes the tool name and a truncated conversation ID for correl
502
502
 
503
503
  Every durable action — cloudflare tunnel-login, brand publish, future deterministic flows — emits a `:Task {kind:"<flow>"}` node carrying the action's lifecycle and a `:PRODUCED` edge to every entity the action created. This makes the graph traversable from the originating Conversation to every entity created during it via `(c)<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e)` — answering "what did this turn produce" in one Cypher hop.
504
504
 
505
- The doctrine is enforced at the storage primitive: writes to `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` MUST include at least one inbound `:PRODUCED` edge whose source is one of `:Task`, `:Conversation`, or `:Message`. Subtype labels like `:AdminConversation`, `:UserMessage`, `:AssistantMessage`, `:AdminMessage` qualify because the gate checks the full `labels()` array. Bootstrap writes (PIN-setup, schema migrations, lazy first-session UserProfile creation) are exempt via `createdBy.agent === 'system'`.
505
+ The doctrine is observed at the storage primitive: writes to `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` should carry an inbound `:PRODUCED` edge whose source is one of `:Task`, `:Conversation`, or `:Message`. Subtype labels like `:AdminConversation`, `:UserMessage`, `:AssistantMessage`, `:AdminMessage` qualify because the gate checks the full `labels()` array. Bootstrap writes (PIN-setup, schema migrations, lazy first-session UserProfile creation) are exempt via `createdBy.agent === 'system'`. When no qualifying edge resolves, the primitive emits a `[graph-write] warn reason=missing-provenance labels=<csv> agent=<agentLabel>` line and the write proceeds (Task 580 relaxed this from a hard reject — the composer-spawned admin path inherits a bare per-account env that never receives the `SESSION_NODE_ID` stamp, so the throw was failing every direct admin contact-create / memory-write for a gated label).
506
506
 
507
507
  Two surfaces emit the lifecycle: agent-driven actions call `work-create`/`work-update`/`work-complete` over MCP (`work-create` accepts `kind`, the canonical `inputsProvided` call-shape record, `inputs` + `inputSchema` for the operator-meaningful form payload, and `raisedDuringConversationKey` to resolve the `RAISED_DURING` edge). Shell-driven actions wrap their script invocation in [platform/ui/app/lib/cloudflare-task-tracker.ts](../../../ui/app/lib/cloudflare-task-tracker.ts) (cloudflare is the first; installer / brand-publish / OAuth-login deferred). Both surfaces emit the same `[task] action-start|step|done` log lines so operators can grep one channel uniformly. Both also call the central `redactSecrets` primitive ([platform/lib/task-secrets/](../../../lib/task-secrets/)) to strip schema-tagged secret keys before persisting `inputs.<field>` props on the Task — see `.docs/neo4j.md § Audit Task input contract` for the contract that replaces per-kind allow-lists.
508
508
 
@@ -108,7 +108,7 @@ Each session row also carries a small muted timestamp crumb under the name showi
108
108
 
109
109
  **Two spawn surfaces, one primitive (Task 573).** The manager runs two on-device spawn surfaces, both backed by the same primitive: **node-pty wrapped in `systemd-run --user --scope`** (via `index.ts::spawnPtyAdapter`).
110
110
 
111
- - **`claude rc` daemon** — spawned at platform boot by `rc-daemon.ts`. One supervised daemon per account; owns the long-lived composer session that backs claude.ai/code Remote Control. Master fd held for the daemon's lifetime, released on natural exit / restart.
111
+ - **`claude rc` daemon** — spawned at platform boot by `rc-daemon.ts`. One supervised daemon per account; owns the long-lived composer session that backs claude.ai/code Remote Control. Master fd held for the daemon's lifetime, released on natural exit / restart. **Headless consent pre-seed (Task 578).** Before the first spawn, `ensureRemoteControlConsent` writes `{"remoteControlAtStartup": true}` into `$CLAUDE_CONFIG_DIR/.claude.json` (read-merge-write, atomic tmp+rename, idempotent). Without this, headless `claude rc` hangs at `Enable Remote Control? (y/n)` — nothing answers, the supervisor restarts the child, eventually marks the daemon permanently-failed. The key is the same one `claude` itself writes when the user answers `y` at the prompt; siblings (`teammateMode`, `hasUsedRemoteControl`, claude's auth blocks) are preserved.
112
112
  - **`claude --remote-control` on-device sidebar spawn** — spawned per-click by `/rc-spawn` in `http-server.ts`. One PTY per click; the manager holds the master fd **for the session's entire lifetime**. The pty master IS the live session — claude operates on the slave, and closing the master hangs up the slave. Valid master-release points: (1) explicit operator teardown — `/stop` → `stopSession` → `op=archive-release` — and (2) the natural-exit path inside `pty.onExit → handlePtyNaturalExit`.
113
113
 
114
114
  Inside the scope, `sh -c 'trap "" HUP; exec "$@"' sh <claudeBin> <args...>` keeps claude resident across PTY master-close (SIGHUP trap) and preserves the pid through the exec chain. The earlier `script(1)` wrap and the non-PTY scope primitive (Tasks 552/556/562) are gone; node-pty allocates the TTY directly.
@@ -132,7 +132,7 @@ Inside the scope, `sh -c 'trap "" HUP; exec "$@"' sh <claudeBin> <args...>` keep
132
132
 
133
133
  `verified=true` requires `master-fd=closed` AND `fdDelta>=1` AND `trackerRemoved=true`. `master-fd=close-failed` is logged at error level (`[rc-spawn-error]` prefix) — never swallowed; the next post-archive sweep is the catch-net.
134
134
 
135
- **Cross-arm `[rc-life]` schema.** rc-spawn and rc-daemon emit a shared log shape so the populations can be compared from `server.log` alone. One spawn's full lifeline is `grep <unitToken>`; one surface's signature is `grep 'source=rc-spawn'` or `'source=rc-daemon'`. Success on both surfaces is `op=pidfile-present`; failure is `op=spawn-failed` / `op=early-exit` / `op=wait-pid-failed`. Full schema and operator runbook in [`.docs/rc-life-observability.md`](../../../.docs/rc-life-observability.md).
135
+ **Cross-arm `[rc-life]` schema.** rc-spawn and rc-daemon emit a shared log shape so the populations can be compared from `server.log` alone. One spawn's full lifeline is `grep <unitToken>`; one surface's signature is `grep 'source=rc-spawn'` or `'source=rc-daemon'`. Success on both surfaces is `op=pidfile-present`; failure is `op=spawn-failed` / `op=early-exit` / `op=wait-pid-failed`. Full schema and operator runbook in [`.docs/rc-life-observability.md`](../../../.docs/rc-life-observability.md). **Measured `remoteBound` (Task 578).** The rc-daemon liveness emit reports `remoteBound` as a measured value flipped by `detectRcHandshake` once the daemon's own post-bind output (`Capacity:` header or an `N of M` capacity line) is seen on the PTY. A daemon that is alive but not registered to Remote Control therefore prints `pidAlive=true remoteBound=false` — previously masked by a hardcoded literal. A class-guard test (`rc-life-literals.test.ts`) scans every `emitRcLife` call across the manager and fails if any status-shaped field is set to a boolean/string literal. The captured PTY output is now dumped on **every** exit (not only fast exits), prefix `exit-output` or `fast-exit-output`, so a late-life prompt-hang is no longer invisible.
136
136
 
137
137
  **Post-archive fd sweep (Task 558).** Independent of spawn/archive request traffic, the manager runs a 60 s sweep that walks both directions of the master-fd invariant:
138
138
 
@@ -89,6 +89,13 @@ tail -200 ~/.maxy/logs/maxy-ui.log | rg '\[remote-auth\].*resolvedKind='
89
89
  2. `diff <(jq .claudeAiOauth.accessToken ~/.maxy/.claude/.credentials.json) <(jq .claudeAiOauth.accessToken ~/.realagent/.claude/.credentials.json)` — must be non-empty after each brand's operator has run `claude /login` against distinct Anthropic accounts; if it's empty, both brands are still logged in to the same account (operator action, not a code bug).
90
90
  3. `grep "\[install\] claude-creds pickup" ~/.${brand}/logs/install-*.log` — fires once on the first post-Task-923 install of any brand and moves the legacy `~/.claude/.credentials.json` into that brand's path. Subsequent brands install with no credentials and require a fresh `claude /login` inside that brand's chat (which writes to the brand-scoped path because the systemd unit env is in scope).
91
91
 
92
+ **All sessions on the brand stopped responding after a token expiry.** Symptom on the operator side: every spawn dies at `pid-file-timeout` and the dashboard health probe reports auth dead. Diagnose the OAuth refresh path before anything else:
93
+
94
+ 1. `tail -n 300 ~/.${brand}/logs/server.log | grep -E 'auth-refresh|auth-health|invalid_grant'` — `op=lock-acquired` proves the cross-process lock is in play (Task 576). `op=skipped-fresh` means a sibling process (the admin server or a `claude` binary) already rotated the tokens during the lock wait — expected, healthy. `op=renewed expiresAt=…` is the only line that means a network refresh actually ran.
95
+ 2. `outcome=fail-token` or `invalid_grant` lines mean Anthropic rejected the refresh token itself (revoked or expired beyond the rotation window). The brand needs a fresh `claude /login`. Pre-576 the most common cause was the admin server and a spawned `claude` racing to rotate the same single-use refresh token; that race is now serialised by the file lock at `~/.${brand}/.claude/.credentials.json.lock` and a re-read after the lock skips redundant refreshes.
96
+ 3. `grep '\[auth-health\]' ~/.${brand}/logs/server.log | tail -n 5` — the heartbeat fires every five minutes. `status=dead expiresIn=...` means the refresh token is gone; only a re-login fixes it. `status=ok` heartbeats with no spawns in between mean the credentials file is healthy and the failure lives elsewhere.
97
+ 4. The spawn-failure surface now carries `reason=auth-refresh-failed` (with `authStatus` in the JSON body) instead of generic `pid-file-timeout` whenever the credentials file is in `dead` or `expired` state at the moment of failure — visible in `grep '\[spawn-failed\]'` on server.log.
98
+
92
99
  ---
93
100
 
94
101
  ## Memory Not Working
@@ -175,7 +175,7 @@ Every node in the graph sits under a parent in the canonical chain `LocalBusines
175
175
 
176
176
  ## Anchoring writes to provenance — `producedByTaskId` and the conversation env-stamp
177
177
 
178
- Writes targeting `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` require an inbound `:PRODUCED` edge whose source is one of `:Task`, `:Conversation`, or `:Message` (deterministic bootstrap paths run as `createdBy.agent === 'system'` and are exempt). Two surfaces feed the gate:
178
+ Writes targeting `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` expect an inbound `:PRODUCED` edge whose source is one of `:Task`, `:Conversation`, or `:Message` (deterministic bootstrap paths run as `createdBy.agent === 'system'` and are exempt). The expectation is enforced as a soft warning, not a reject: when no qualifying edge resolves the storage primitive emits a single `[graph-write] warn reason=missing-provenance labels=<csv> agent=<agentLabel>` line on stderr and the write proceeds. Task 580 relaxed this from a hard throw — the composer-spawned admin path inherits a bare per-account env that never receives the `SESSION_NODE_ID` stamp, so the throw was failing every direct admin contact-create / memory-write for a gated label. The two stamping surfaces below remain the right thing to use; the warn line is the operator-visible signal for how often the gap fires. Two surfaces feed the stamp:
179
179
 
180
180
  - **Workflow path — `producedByTaskId`.** `memory-write` accepts an optional `producedByTaskId` parameter. When set, the platform composes an inbound `:PRODUCED` edge from that `:Task` node into the write's `relationships` array before the write runs. The Task and the new node must share the same `accountId`; mismatch is rejected loud. The typical pattern: call `work-create` at the start of an autonomous flow (onboarding skill, cloudflare tunnel-login) with `kind` and `raisedDuringConversationKey`; capture the returned `taskId`; pass it as `producedByTaskId` on every subsequent `memory-write` for one of the gated labels.
181
181
 
@@ -99,8 +99,8 @@ graph as a single action.
99
99
  **Never run `cypher-shell` via `Bash` to work around the read-only tool.**
100
100
  The admin session's `Bash` surface has no path to the per-brand Neo4j
101
101
  password (it lives in `${PLATFORM_ROOT}/config/.neo4j-password`,
102
- unreachable from the admin account's `Bash` cwd, and pre-tool-use hooks
103
- block `Bash` against the code tree anyway — `.docs/neo4j.md`'s migration
102
+ unreachable from the admin account's `Bash` cwd, and IDENTITY.md doctrine
103
+ forbids `Bash` against the code tree anyway — `.docs/neo4j.md`'s migration
104
104
  section is an operator-run runbook, not a path you should replicate in
105
105
  chat). Every write intent has a schema-aware tool above. If none of them
106
106
  fits, tell the user plainly: *"This operation is not in the admin tool
@@ -1,12 +1,13 @@
1
1
  export interface UrlGetParams {
2
2
  url: string;
3
3
  }
4
- export type UrlGetError = "fetch" | "http" | "non-html" | "challenge" | "empty" | "no-account-dir";
4
+ export type UrlGetError = "fetch" | "http" | "non-html" | "challenge" | "no-account-dir";
5
5
  export type UrlGetResult = {
6
6
  ok: true;
7
7
  url: string;
8
8
  httpStatus: number;
9
9
  bytes: number;
10
+ textLength: number;
10
11
  referencePath: string;
11
12
  summary: string;
12
13
  summarized: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"url-get.d.ts","sourceRoot":"","sources":["../../src/tools/url-get.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEnG,MAAM,MAAM,YAAY,GACpB;IACE,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;CACrB,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAyCN,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAqGxE"}
1
+ {"version":3,"file":"url-get.d.ts","sourceRoot":"","sources":["../../src/tools/url-get.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAEzF,MAAM,MAAM,YAAY,GACpB;IACE,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;CACrB,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAyCN,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA2GxE"}
@@ -98,16 +98,11 @@ export async function urlGet(params) {
98
98
  };
99
99
  }
100
100
  const markdown = NodeHtmlMarkdown.translate(html).trim();
101
- if (markdown.length === 0) {
102
- console.error(`[url-get] url=${url} error=empty`);
103
- return {
104
- ok: false,
105
- url,
106
- error: "empty",
107
- httpStatus,
108
- message: "page has no readable text content",
109
- };
110
- }
101
+ const textLength = markdown.length;
102
+ // HTTP 200 is success. Empty extractable text is a property of the response,
103
+ // not an error: a JS-rendered SPA shell legitimately has zero markdown bytes
104
+ // until the bundle runs. Surface `textLength` so the caller can decide
105
+ // whether to fall back to browser-render; never invert the success signal.
111
106
  const accountDir = resolveAccountDir();
112
107
  if (!accountDir) {
113
108
  console.error(`[url-get] url=${url} error=no-account-dir`);
@@ -128,20 +123,31 @@ export async function urlGet(params) {
128
123
  await writeFile(referencePath, fileBody, "utf-8");
129
124
  // Summary is derived and best-effort. The verbatim file is the contract and
130
125
  // is already on disk; a summary failure degrades gracefully rather than
131
- // discarding the artefact.
126
+ // discarding the artefact. On empty extractable text we short-circuit with a
127
+ // synthesised summary — calling the LLM on zero input either fails or
128
+ // hallucinates, neither of which serves the agent.
132
129
  let summary;
133
130
  let summarized;
134
- try {
135
- summary = await summarise(url, markdown.slice(0, SUMMARY_INPUT_CAP));
136
- summarized = true;
137
- }
138
- catch (err) {
139
- const detail = err instanceof Error ? err.message : String(err);
140
- summary = `<summary unavailable: ${detail}> — read the reference file at ${referencePath} for the full text.`;
131
+ if (textLength === 0) {
132
+ summary =
133
+ `Page returned HTTP ${httpStatus} but had no extractable text content ` +
134
+ `(textLength=0; bytes=${bytes}). The response is likely a JavaScript-` +
135
+ `rendered shell; use browser-render to retrieve the rendered DOM.`;
141
136
  summarized = false;
142
137
  }
143
- console.error(`[url-get] url=${url} httpStatus=${httpStatus} bytes=${bytes} refPath=${referencePath} ` +
144
- `summarized=${summarized} (${Date.now() - t0}ms)`);
145
- return { ok: true, url, httpStatus, bytes, referencePath, summary, summarized };
138
+ else {
139
+ try {
140
+ summary = await summarise(url, markdown.slice(0, SUMMARY_INPUT_CAP));
141
+ summarized = true;
142
+ }
143
+ catch (err) {
144
+ const detail = err instanceof Error ? err.message : String(err);
145
+ summary = `<summary unavailable: ${detail}> — read the reference file at ${referencePath} for the full text.`;
146
+ summarized = false;
147
+ }
148
+ }
149
+ console.error(`[url-get] url=${url} httpStatus=${httpStatus} bytes=${bytes} textLength=${textLength} ` +
150
+ `refPath=${referencePath} summarized=${summarized} (${Date.now() - t0}ms)`);
151
+ return { ok: true, url, httpStatus, bytes, textLength, referencePath, summary, summarized };
146
152
  }
147
153
  //# sourceMappingURL=url-get.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"url-get.js","sourceRoot":"","sources":["../../src/tools/url-get.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"url-get.js","sourceRoot":"","sources":["../../src/tools/url-get.ts"],"names":[],"mappings":"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"}
@@ -41,6 +41,9 @@ const ALLOWLIST = new Set([
41
41
  // Same vi.hoisted pattern: Task 549's sidebar-sessions test pre-builds a
42
42
  // tmpdir before the module-under-test's ACCOUNTS_DIR mock is wired.
43
43
  'platform/ui/server/routes/admin/__tests__/sidebar-sessions.test.ts',
44
+ // Same vi.hoisted pattern: Task 576's claude-auth refresh-lock test
45
+ // builds a tmpdir + creds path before the module's `../paths` mock fires.
46
+ 'platform/ui/app/lib/__tests__/claude-auth-refresh-lock.test.ts',
44
47
  ])
45
48
 
46
49
  const REQUIRE_CALL_RE = /\brequire\s*\(/
@@ -66,14 +66,12 @@ cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
66
66
  {
67
67
  "matcher": "Write",
68
68
  "hooks": [
69
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" },
70
69
  { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
71
70
  ]
72
71
  },
73
72
  {
74
73
  "matcher": "Edit",
75
74
  "hooks": [
76
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" },
77
75
  { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
78
76
  ]
79
77
  },
@@ -86,22 +84,9 @@ cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
86
84
  {
87
85
  "matcher": "Bash",
88
86
  "hooks": [
89
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" },
90
87
  { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
91
88
  ]
92
89
  },
93
- {
94
- "matcher": "mcp__plugin_memory_memory__memory-write",
95
- "hooks": [
96
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" }
97
- ]
98
- },
99
- {
100
- "matcher": "mcp__plugin_memory_memory__memory-update",
101
- "hooks": [
102
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" }
103
- ]
104
- },
105
90
  {
106
91
  "hooks": [
107
92
  { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
@@ -0,0 +1,4 @@
1
+ export type AuthSnapshot = 'dead' | 'expired' | 'unknown';
2
+ export declare function credentialsPathForConfigDir(claudeConfigDir: string): string;
3
+ export declare function snapshotAuthForFailureReason(credentialsPath: string | undefined): AuthSnapshot;
4
+ //# sourceMappingURL=auth-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-snapshot.d.ts","sourceRoot":"","sources":["../src/auth-snapshot.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAA;AAEzD,wBAAgB,2BAA2B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,4BAA4B,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAqB9F"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Task 576 — auth-snapshot.
3
+ *
4
+ * Read-only, no-dependency snapshot of the OAuth credentials file used
5
+ * by the http-server spawn-failure path to surface `reason=auth-refresh-failed`
6
+ * to the operator instead of the generic `pid-file-timeout`. Lives in
7
+ * claude-session-manager so the failure path stays in one tree; the
8
+ * authoritative refresh + lock logic continues to live in
9
+ * `platform/ui/app/lib/claude-auth.ts`.
10
+ *
11
+ * Returns one of:
12
+ * - `dead` — token expired AND no refresh token available
13
+ * - `expired` — token expired but a refresh token is on disk
14
+ * - `unknown` — token still valid, or the file is missing / unreadable
15
+ * (not auth's fault — let the original failure reason stand)
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ export function credentialsPathForConfigDir(claudeConfigDir) {
20
+ return resolve(claudeConfigDir, '.credentials.json');
21
+ }
22
+ export function snapshotAuthForFailureReason(credentialsPath) {
23
+ if (!credentialsPath)
24
+ return 'unknown';
25
+ let raw;
26
+ try {
27
+ raw = readFileSync(credentialsPath, 'utf-8');
28
+ }
29
+ catch {
30
+ return 'unknown';
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ return 'unknown';
38
+ }
39
+ const oauth = parsed.claudeAiOauth;
40
+ if (!oauth || typeof oauth !== 'object')
41
+ return 'unknown';
42
+ const expiresAt = oauth.expiresAt;
43
+ if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt))
44
+ return 'unknown';
45
+ if (expiresAt > Date.now())
46
+ return 'unknown'; // not the auth path's fault
47
+ const refreshToken = oauth.refreshToken;
48
+ return typeof refreshToken === 'string' && refreshToken.length > 0 ? 'expired' : 'dead';
49
+ }
50
+ //# sourceMappingURL=auth-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-snapshot.js","sourceRoot":"","sources":["../src/auth-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAInC,MAAM,UAAU,2BAA2B,CAAC,eAAuB;IACjE,OAAO,OAAO,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAA;AACtD,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,eAAmC;IAC9E,IAAI,CAAC,eAAe;QAAE,OAAO,SAAS,CAAA;IACtC,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,IAAI,MAA+B,CAAA;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,aAAoD,CAAA;IACzE,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IACzD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;IACjC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAA;IAClF,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,SAAS,CAAA,CAAC,4BAA4B;IACzE,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAA;IACvC,OAAO,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAA;AACzF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAM3B,OAAO,EAgBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAezB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AA+E9E,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAI1D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;CAC/B;AA4LD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAi4CpD"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAM3B,OAAO,EAgBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAezB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAgF9E,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAI1D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;CAC/B;AA4LD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAs5CpD"}
@@ -31,6 +31,7 @@ import { readJsonlSession } from './jsonl-enumerator.js';
31
31
  import { claudeStateRoot, projectSlugForCwd, jsonlPathForSessionId, sidecarPathForSessionId, permissionModeLogPathForSessionId, findExistingJsonlForSessionId, } from './jsonl-path.js';
32
32
  import { basename, join } from 'node:path';
33
33
  import { validateUserTitle } from './user-title-store.js';
34
+ import { credentialsPathForConfigDir, snapshotAuthForFailureReason } from './auth-snapshot.js';
34
35
  const ROLES = ['admin', 'public'];
35
36
  const CHANNELS = ['browser', 'whatsapp', 'telegram', 'webchat', 'email'];
36
37
  // Task 382 — body.conversationNodeId shape gate. The value is the
@@ -672,7 +673,16 @@ export function buildHttpApp(deps) {
672
673
  return c.json({ error: 'operator-slots-reserved', total: result.rejected.total, reserve: result.rejected.reserve, totalCap: result.rejected.totalCap }, 503);
673
674
  }
674
675
  timed(deps.logger, 'POST', '/spawn', 500, Date.now() - start);
675
- return c.json({ error: 'spawn-failed', reason: result.reason, stderrTail: result.stderrTail }, 500);
676
+ // Task 576 — surface the auth-refresh-failed reason when the
677
+ // creds file says `dead` or `expired`. Without this the operator
678
+ // sees a generic `pid-file-timeout` for what is actually an
679
+ // OAuth-token failure and cannot recover via "Reconnect Claude".
680
+ const authSnap = snapshotAuthForFailureReason(credentialsPathForConfigDir(deps.claudeConfigDir));
681
+ const reason = authSnap === 'dead' || authSnap === 'expired' ? 'auth-refresh-failed' : result.reason;
682
+ if (reason === 'auth-refresh-failed') {
683
+ deps.logger(`[spawn-failed] reason=auth-refresh-failed authStatus=${authSnap} originalReason=${result.reason}`);
684
+ }
685
+ return c.json({ error: 'spawn-failed', reason, stderrTail: result.stderrTail, authStatus: authSnap }, 500);
676
686
  }
677
687
  timed(deps.logger, 'POST', '/spawn', 200, Date.now() - start);
678
688
  const payload = buildSpawnReturnPayload(result.session.sessionId, result.session.pid, result.session.startedAt, result.session.url);
@@ -1487,7 +1497,14 @@ export function buildHttpApp(deps) {
1487
1497
  deps.logger(`[sessions-rc-resume] spawn-failed sessionId=${sessionId ?? 'new'} name=${name ?? 'none'} err=${msg}`);
1488
1498
  deps.logger(`[rc-spawn] op=spawn-failed unitToken=${unitToken} err=${JSON.stringify(msg)}`);
1489
1499
  timed(deps.logger, 'POST', '/rc-spawn', 500, Date.now() - start);
1490
- return c.json({ error: msg }, 500);
1500
+ // Task 576 same auth-aware reason as /spawn so the resume path
1501
+ // surfaces auth death too, not only spawn-time failures.
1502
+ const authSnap = snapshotAuthForFailureReason(credentialsPathForConfigDir(deps.claudeConfigDir));
1503
+ const reason = authSnap === 'dead' || authSnap === 'expired' ? 'auth-refresh-failed' : msg;
1504
+ if (reason === 'auth-refresh-failed') {
1505
+ deps.logger(`[rc-spawn] op=auth-refresh-failed authStatus=${authSnap} unitToken=${unitToken}`);
1506
+ }
1507
+ return c.json({ error: msg, reason, authStatus: authSnap }, 500);
1491
1508
  }
1492
1509
  // Step 3 — PTY spawned. fd baseline at this instant; the fd-release
1493
1510
  // line later in the lifecycle is read relative to this value.