@rubytech/create-maxy-code 0.1.605 → 0.1.606

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 (207) hide show
  1. package/dist/__tests__/claude-trust-seed.test.js +125 -0
  2. package/dist/claude-trust-seed.js +52 -0
  3. package/dist/index.js +68 -33
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
  6. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +5 -0
  7. package/payload/platform/plugins/docs/references/admin-ui.md +13 -0
  8. package/payload/platform/plugins/docs/references/platform.md +2 -0
  9. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.d.ts +34 -0
  10. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.d.ts.map +1 -0
  11. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.js +83 -0
  12. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.js.map +1 -0
  13. package/payload/platform/services/claude-session-manager/dist/index.js +13 -0
  14. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  15. package/payload/server/public/activity.html +6 -6
  16. package/payload/server/public/agents.html +5 -5
  17. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js +1 -0
  18. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js.br +0 -0
  19. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js.gz +0 -0
  20. package/payload/server/public/assets/AdminShell-1HyATV_t.js +3 -0
  21. package/payload/server/public/assets/AdminShell-1HyATV_t.js.br +0 -0
  22. package/payload/server/public/assets/AdminShell-1HyATV_t.js.gz +0 -0
  23. package/payload/server/public/assets/activity-C2qWlcJ5.js +1 -0
  24. package/payload/server/public/assets/activity-C2qWlcJ5.js.br +0 -0
  25. package/payload/server/public/assets/activity-C2qWlcJ5.js.gz +0 -0
  26. package/payload/server/public/assets/admin-DqpfY8-t.js +1 -0
  27. package/payload/server/public/assets/admin-DqpfY8-t.js.br +0 -0
  28. package/payload/server/public/assets/admin-DqpfY8-t.js.gz +0 -0
  29. package/payload/server/public/assets/agents-DoGVAjIT.js +1 -0
  30. package/payload/server/public/assets/agents-DoGVAjIT.js.br +0 -0
  31. package/payload/server/public/assets/agents-DoGVAjIT.js.gz +0 -0
  32. package/payload/server/public/assets/{browser-CqDBcKwU.js → browser-DjqS897k.js} +1 -1
  33. package/payload/server/public/assets/browser-DjqS897k.js.br +0 -0
  34. package/payload/server/public/assets/browser-DjqS897k.js.gz +0 -0
  35. package/payload/server/public/assets/calendar-DJeOm0WS.js +1 -0
  36. package/payload/server/public/assets/calendar-DJeOm0WS.js.br +0 -0
  37. package/payload/server/public/assets/calendar-DJeOm0WS.js.gz +0 -0
  38. package/payload/server/public/assets/chat-BrRE0Dck.js +1 -0
  39. package/payload/server/public/assets/chat-BrRE0Dck.js.br +0 -0
  40. package/payload/server/public/assets/chat-BrRE0Dck.js.gz +0 -0
  41. package/payload/server/public/assets/chevron-left-D9z1aK3p.js +1 -0
  42. package/payload/server/public/assets/chevron-left-D9z1aK3p.js.br +0 -0
  43. package/payload/server/public/assets/clock-BqHWMICF.js +1 -0
  44. package/payload/server/public/assets/clock-BqHWMICF.js.br +0 -0
  45. package/payload/server/public/assets/clock-BqHWMICF.js.gz +0 -0
  46. package/payload/server/public/assets/copy-p9ZM9fnn.js +1 -0
  47. package/payload/server/public/assets/copy-p9ZM9fnn.js.br +0 -0
  48. package/payload/server/public/assets/copy-p9ZM9fnn.js.gz +0 -0
  49. package/payload/server/public/assets/data-CPsqW0qW.js +1 -0
  50. package/payload/server/public/assets/data-CPsqW0qW.js.br +0 -0
  51. package/payload/server/public/assets/data-CPsqW0qW.js.gz +0 -0
  52. package/payload/server/public/assets/{field-X4OVA3vx.js → field-WRk67lvr.js} +1 -1
  53. package/payload/server/public/assets/field-WRk67lvr.js.br +0 -0
  54. package/payload/server/public/assets/field-WRk67lvr.js.gz +0 -0
  55. package/payload/server/public/assets/file-text-BJDCb334.js +1 -0
  56. package/payload/server/public/assets/file-text-BJDCb334.js.br +0 -0
  57. package/payload/server/public/assets/file-text-BJDCb334.js.gz +0 -0
  58. package/payload/server/public/assets/{graph-l7tzHqvm.js → graph-CAU2xh2Z.js} +2 -2
  59. package/payload/server/public/assets/graph-CAU2xh2Z.js.br +0 -0
  60. package/payload/server/public/assets/graph-CAU2xh2Z.js.gz +0 -0
  61. package/payload/server/public/assets/graph-labels-CEDmX7qW.js +1 -0
  62. package/payload/server/public/assets/graph-labels-CEDmX7qW.js.br +0 -0
  63. package/payload/server/public/assets/graph-labels-CEDmX7qW.js.gz +0 -0
  64. package/payload/server/public/assets/{jsx-runtime-bq0g4NlA.css → jsx-runtime-hPFGo4rC.css} +1 -1
  65. package/payload/server/public/assets/jsx-runtime-hPFGo4rC.css.br +0 -0
  66. package/payload/server/public/assets/{jsx-runtime-bq0g4NlA.css.gz → jsx-runtime-hPFGo4rC.css.gz} +0 -0
  67. package/payload/server/public/assets/{operator-Dg-dwf7a.js → operator-1WFztEQA.js} +1 -1
  68. package/payload/server/public/assets/operator-1WFztEQA.js.br +0 -0
  69. package/payload/server/public/assets/operator-1WFztEQA.js.gz +0 -0
  70. package/payload/server/public/assets/page-B72XigcU.js +32 -0
  71. package/payload/server/public/assets/page-B72XigcU.js.br +0 -0
  72. package/payload/server/public/assets/page-B72XigcU.js.gz +0 -0
  73. package/payload/server/public/assets/page-nT8l_cX1.js +1 -0
  74. package/payload/server/public/assets/page-nT8l_cX1.js.br +0 -0
  75. package/payload/server/public/assets/page-nT8l_cX1.js.gz +0 -0
  76. package/payload/server/public/assets/play-CYqOCqm6.js +1 -0
  77. package/payload/server/public/assets/play-CYqOCqm6.js.br +0 -0
  78. package/payload/server/public/assets/play-CYqOCqm6.js.gz +0 -0
  79. package/payload/server/public/assets/public-DAea6w7w.js +1 -0
  80. package/payload/server/public/assets/public-DAea6w7w.js.br +0 -0
  81. package/payload/server/public/assets/public-DAea6w7w.js.gz +0 -0
  82. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js +1 -0
  83. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js.br +0 -0
  84. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js.gz +0 -0
  85. package/payload/server/public/assets/{routines-6fxZNacQ.js → routines-vy5xsyqx.js} +2 -2
  86. package/payload/server/public/assets/routines-vy5xsyqx.js.br +0 -0
  87. package/payload/server/public/assets/routines-vy5xsyqx.js.gz +0 -0
  88. package/payload/server/public/assets/{skills-51mqx-s5.js → skills-X7FTilVy.js} +1 -1
  89. package/payload/server/public/assets/skills-X7FTilVy.js.br +0 -0
  90. package/payload/server/public/assets/skills-X7FTilVy.js.gz +0 -0
  91. package/payload/server/public/assets/tasks-BXx_jk_p.js +3 -0
  92. package/payload/server/public/assets/tasks-BXx_jk_p.js.br +0 -0
  93. package/payload/server/public/assets/tasks-BXx_jk_p.js.gz +0 -0
  94. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js +1 -0
  95. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js.br +0 -0
  96. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js.gz +0 -0
  97. package/payload/server/public/assets/{useCopyFeedback-BG7h37Rx.js → useCopyFeedback-DLSKs6r2.js} +1 -1
  98. package/payload/server/public/assets/useCopyFeedback-DLSKs6r2.js.br +0 -0
  99. package/payload/server/public/assets/useCopyFeedback-DLSKs6r2.js.gz +0 -0
  100. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js +6 -0
  101. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js.br +0 -0
  102. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js.gz +0 -0
  103. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js +2 -0
  104. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js.br +0 -0
  105. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js.gz +0 -0
  106. package/payload/server/public/assets/wrench-nDMQ6x-6.js +1 -0
  107. package/payload/server/public/assets/wrench-nDMQ6x-6.js.br +0 -0
  108. package/payload/server/public/assets/wrench-nDMQ6x-6.js.gz +0 -0
  109. package/payload/server/public/browser.html +5 -5
  110. package/payload/server/public/calendar.html +8 -8
  111. package/payload/server/public/chat.html +14 -14
  112. package/payload/server/public/data.html +12 -12
  113. package/payload/server/public/field.html +3 -3
  114. package/payload/server/public/graph.html +10 -10
  115. package/payload/server/public/index.html +14 -14
  116. package/payload/server/public/operator.html +16 -16
  117. package/payload/server/public/public.html +14 -14
  118. package/payload/server/public/routines.html +8 -8
  119. package/payload/server/public/skills.html +6 -6
  120. package/payload/server/public/tasks.html +7 -7
  121. package/payload/server/server.js +1 -1
  122. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js +0 -1
  123. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js.br +0 -0
  124. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js.gz +0 -0
  125. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js +0 -3
  126. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js.br +0 -0
  127. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js.gz +0 -0
  128. package/payload/server/public/assets/activity-CqgFwANo.js +0 -1
  129. package/payload/server/public/assets/activity-CqgFwANo.js.br +0 -0
  130. package/payload/server/public/assets/activity-CqgFwANo.js.gz +0 -0
  131. package/payload/server/public/assets/admin-K63n6ybW.js +0 -1
  132. package/payload/server/public/assets/admin-K63n6ybW.js.br +0 -0
  133. package/payload/server/public/assets/admin-K63n6ybW.js.gz +0 -0
  134. package/payload/server/public/assets/agents-cue5Zf8X.js +0 -1
  135. package/payload/server/public/assets/agents-cue5Zf8X.js.br +0 -0
  136. package/payload/server/public/assets/agents-cue5Zf8X.js.gz +0 -0
  137. package/payload/server/public/assets/browser-CqDBcKwU.js.br +0 -0
  138. package/payload/server/public/assets/browser-CqDBcKwU.js.gz +0 -0
  139. package/payload/server/public/assets/calendar-BgzuW5Rt.js +0 -1
  140. package/payload/server/public/assets/calendar-BgzuW5Rt.js.br +0 -0
  141. package/payload/server/public/assets/calendar-BgzuW5Rt.js.gz +0 -0
  142. package/payload/server/public/assets/chat-HYZViu2u.js +0 -1
  143. package/payload/server/public/assets/chat-HYZViu2u.js.br +0 -3
  144. package/payload/server/public/assets/chat-HYZViu2u.js.gz +0 -0
  145. package/payload/server/public/assets/chevron-left-C5FXH9r_.js +0 -1
  146. package/payload/server/public/assets/chevron-left-C5FXH9r_.js.br +0 -0
  147. package/payload/server/public/assets/clock-DxPtyejC.js +0 -1
  148. package/payload/server/public/assets/clock-DxPtyejC.js.br +0 -0
  149. package/payload/server/public/assets/clock-DxPtyejC.js.gz +0 -0
  150. package/payload/server/public/assets/copy-BNhZ0flH.js +0 -1
  151. package/payload/server/public/assets/copy-BNhZ0flH.js.br +0 -0
  152. package/payload/server/public/assets/copy-BNhZ0flH.js.gz +0 -0
  153. package/payload/server/public/assets/data-kCBB8Z4Z.js +0 -1
  154. package/payload/server/public/assets/data-kCBB8Z4Z.js.br +0 -0
  155. package/payload/server/public/assets/data-kCBB8Z4Z.js.gz +0 -0
  156. package/payload/server/public/assets/field-X4OVA3vx.js.br +0 -0
  157. package/payload/server/public/assets/field-X4OVA3vx.js.gz +0 -0
  158. package/payload/server/public/assets/file-text-BddO_2EI.js +0 -1
  159. package/payload/server/public/assets/file-text-BddO_2EI.js.br +0 -0
  160. package/payload/server/public/assets/file-text-BddO_2EI.js.gz +0 -0
  161. package/payload/server/public/assets/graph-l7tzHqvm.js.br +0 -0
  162. package/payload/server/public/assets/graph-l7tzHqvm.js.gz +0 -0
  163. package/payload/server/public/assets/graph-labels-CtFFq5W2.js +0 -1
  164. package/payload/server/public/assets/graph-labels-CtFFq5W2.js.br +0 -0
  165. package/payload/server/public/assets/graph-labels-CtFFq5W2.js.gz +0 -0
  166. package/payload/server/public/assets/jsx-runtime-bq0g4NlA.css.br +0 -0
  167. package/payload/server/public/assets/operator-Dg-dwf7a.js.br +0 -7
  168. package/payload/server/public/assets/operator-Dg-dwf7a.js.gz +0 -0
  169. package/payload/server/public/assets/page-Ci1bfIbs.js +0 -1
  170. package/payload/server/public/assets/page-Ci1bfIbs.js.br +0 -0
  171. package/payload/server/public/assets/page-Ci1bfIbs.js.gz +0 -0
  172. package/payload/server/public/assets/page-Y9JmFq0Q.js +0 -32
  173. package/payload/server/public/assets/page-Y9JmFq0Q.js.br +0 -0
  174. package/payload/server/public/assets/page-Y9JmFq0Q.js.gz +0 -0
  175. package/payload/server/public/assets/play-BSZVGpQT.js +0 -1
  176. package/payload/server/public/assets/play-BSZVGpQT.js.br +0 -0
  177. package/payload/server/public/assets/play-BSZVGpQT.js.gz +0 -0
  178. package/payload/server/public/assets/public-m66FAza_.js +0 -1
  179. package/payload/server/public/assets/public-m66FAza_.js.br +0 -0
  180. package/payload/server/public/assets/public-m66FAza_.js.gz +0 -0
  181. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js +0 -1
  182. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js.br +0 -0
  183. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js.gz +0 -0
  184. package/payload/server/public/assets/routines-6fxZNacQ.js.br +0 -0
  185. package/payload/server/public/assets/routines-6fxZNacQ.js.gz +0 -0
  186. package/payload/server/public/assets/skills-51mqx-s5.js.br +0 -0
  187. package/payload/server/public/assets/skills-51mqx-s5.js.gz +0 -0
  188. package/payload/server/public/assets/tasks-D4y-Cwa3.js +0 -3
  189. package/payload/server/public/assets/tasks-D4y-Cwa3.js.br +0 -0
  190. package/payload/server/public/assets/tasks-D4y-Cwa3.js.gz +0 -0
  191. package/payload/server/public/assets/triangle-alert-BzCjDFts.js +0 -1
  192. package/payload/server/public/assets/triangle-alert-BzCjDFts.js.br +0 -0
  193. package/payload/server/public/assets/triangle-alert-BzCjDFts.js.gz +0 -0
  194. package/payload/server/public/assets/useCopyFeedback-BG7h37Rx.js.br +0 -2
  195. package/payload/server/public/assets/useCopyFeedback-BG7h37Rx.js.gz +0 -0
  196. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js +0 -6
  197. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js.br +0 -0
  198. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js.gz +0 -0
  199. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js +0 -2
  200. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js.br +0 -0
  201. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js.gz +0 -0
  202. package/payload/server/public/assets/wrench-B2WKLLQG.js +0 -1
  203. package/payload/server/public/assets/wrench-B2WKLLQG.js.br +0 -0
  204. package/payload/server/public/assets/wrench-B2WKLLQG.js.gz +0 -0
  205. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js → jsx-runtime-CvVXOszc.js} +0 -0
  206. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js.br → jsx-runtime-CvVXOszc.js.br} +0 -0
  207. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js.gz → jsx-runtime-CvVXOszc.js.gz} +0 -0
@@ -0,0 +1,125 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { seedClaudeTrust, formatTrustSeedLine } from "../claude-trust-seed.js";
4
+ const ROOT = "/home/neo/maxy-code/data/accounts";
5
+ const A = `${ROOT}/aaa`;
6
+ const B = `${ROOT}/bbb`;
7
+ test("seeds every account directory that exists, alongside $HOME", () => {
8
+ const r = seedClaudeTrust({
9
+ state: {},
10
+ homeWorkspace: "/home/neo",
11
+ accountsRoot: ROOT,
12
+ accountDirs: [A, B],
13
+ });
14
+ const projects = r.state.projects;
15
+ assert.equal(projects["/home/neo"].hasTrustDialogAccepted, true);
16
+ assert.equal(projects[A].hasTrustDialogAccepted, true);
17
+ assert.equal(projects[B].hasTrustDialogAccepted, true);
18
+ assert.equal(r.state.hasCompletedOnboarding, true);
19
+ assert.deepEqual({ accounts: r.accounts, seeded: r.seeded, alreadyTrusted: r.alreadyTrusted, stale: r.stale }, { accounts: 2, seeded: 2, alreadyTrusted: 0, stale: 0 });
20
+ });
21
+ test("preserves other keys on an account entry and counts an existing true as already-set", () => {
22
+ const r = seedClaudeTrust({
23
+ state: {
24
+ numStartups: 519,
25
+ projects: {
26
+ [A]: { hasTrustDialogAccepted: true, exampleFilesGenerated: true },
27
+ [B]: { hasTrustDialogAccepted: false, lastCost: 1.5 },
28
+ },
29
+ },
30
+ homeWorkspace: "/home/neo",
31
+ accountsRoot: ROOT,
32
+ accountDirs: [A, B],
33
+ });
34
+ const projects = r.state.projects;
35
+ assert.equal(r.state.numStartups, 519);
36
+ assert.equal(projects[A].exampleFilesGenerated, true);
37
+ assert.equal(projects[B].lastCost, 1.5);
38
+ assert.equal(projects[B].hasTrustDialogAccepted, true);
39
+ assert.equal(r.alreadyTrusted, 1);
40
+ assert.equal(r.seeded, 1);
41
+ });
42
+ test("an entry whose directory is gone keeps its value and counts as stale", () => {
43
+ const gone = `${ROOT}/deleted`;
44
+ const r = seedClaudeTrust({
45
+ state: { projects: { [gone]: { hasTrustDialogAccepted: false } } },
46
+ homeWorkspace: "/home/neo",
47
+ accountsRoot: ROOT,
48
+ accountDirs: [A],
49
+ });
50
+ const projects = r.state.projects;
51
+ assert.equal(projects[gone].hasTrustDialogAccepted, false);
52
+ assert.equal(r.stale, 1);
53
+ assert.equal(r.accounts, 1);
54
+ });
55
+ // The CLI records an entry for any directory a session ran in, so an account's
56
+ // own subdirectories sit under the accounts root too. Counting them as stale
57
+ // accounts would inflate the field for every operator who worked inside one.
58
+ test("a subdirectory of a live account is not counted as a stale account", () => {
59
+ const r = seedClaudeTrust({
60
+ state: {
61
+ projects: {
62
+ [A]: { hasTrustDialogAccepted: true },
63
+ [`${A}/sites/portal`]: { hasTrustDialogAccepted: true },
64
+ },
65
+ },
66
+ homeWorkspace: "/home/neo",
67
+ accountsRoot: ROOT,
68
+ accountDirs: [A],
69
+ });
70
+ assert.equal(r.stale, 0);
71
+ });
72
+ test("no account directories is accounts=0, not a throw", () => {
73
+ const r = seedClaudeTrust({
74
+ state: {},
75
+ homeWorkspace: "/home/neo",
76
+ accountsRoot: ROOT,
77
+ accountDirs: [],
78
+ });
79
+ assert.equal(r.accounts, 0);
80
+ assert.equal(r.state.projects["/home/neo"] !== undefined, true);
81
+ });
82
+ // deployPayload() runs before setupAccount(), so on a FRESH install the first
83
+ // pass sees no accounts directory at all and the account that setupAccount
84
+ // then creates would never be trusted. The installer therefore primes twice;
85
+ // this pins the shape that second pass depends on.
86
+ test("fresh-install shape: an empty first pass then a second pass seeds the new account", () => {
87
+ const first = seedClaudeTrust({
88
+ state: {},
89
+ homeWorkspace: "/home/neo",
90
+ accountsRoot: ROOT,
91
+ accountDirs: [],
92
+ });
93
+ assert.equal(first.accounts, 0);
94
+ assert.equal(first.seeded, 0);
95
+ const second = seedClaudeTrust({
96
+ state: first.state,
97
+ homeWorkspace: "/home/neo",
98
+ accountsRoot: ROOT,
99
+ accountDirs: [A],
100
+ });
101
+ const projects = second.state.projects;
102
+ assert.equal(projects[A].hasTrustDialogAccepted, true);
103
+ assert.equal(second.accounts, 1);
104
+ assert.equal(second.seeded, 1);
105
+ assert.equal(projects["/home/neo"].hasTrustDialogAccepted, true);
106
+ });
107
+ test("the install line names which pass ran", () => {
108
+ const r = seedClaudeTrust({
109
+ state: {},
110
+ homeWorkspace: "/home/neo",
111
+ accountsRoot: ROOT,
112
+ accountDirs: [A],
113
+ });
114
+ const line = formatTrustSeedLine("/cfg/.claude.json", "/home/neo", r, "none", "post-account");
115
+ assert.equal(line, " [install] op=claude-trust-seed phase=post-account file=/cfg/.claude.json home=/home/neo" +
116
+ " accounts=1 seeded=1 alreadyTrusted=0 stale=0 error=none");
117
+ });
118
+ test("second run over the first run's output is byte-identical", () => {
119
+ const input = { state: {}, homeWorkspace: "/home/neo", accountsRoot: ROOT, accountDirs: [A, B] };
120
+ const first = seedClaudeTrust(input);
121
+ const second = seedClaudeTrust({ ...input, state: first.state });
122
+ assert.equal(JSON.stringify(second.state), JSON.stringify(first.state));
123
+ assert.equal(second.seeded, 0);
124
+ assert.equal(second.alreadyTrusted, 2);
125
+ });
@@ -0,0 +1,52 @@
1
+ // Task 2720 — Claude Code scopes folder trust to the git repository root, and
2
+ // provision-account-dir.sh gives every account directory its own repo, so
3
+ // trusting $HOME does not reach them. An untrusted workspace halts the
4
+ // PTY-spawned `claude --remote-control` on the folder-trust dialog before it
5
+ // binds, which reads downstream as `op=bind-signal-absent … pidFileSeen=false`
6
+ // and, because /rc-spawn answers 200, reaches the operator as silence.
7
+ //
8
+ // Pure so the counts and the preservation invariant are testable without a
9
+ // filesystem; the caller owns the read and the write.
10
+ export function seedClaudeTrust(input) {
11
+ const state = input.state;
12
+ state.hasCompletedOnboarding = true;
13
+ state.theme = state.theme ?? "auto";
14
+ const projects = (state.projects ?? {});
15
+ const homeEntry = projects[input.homeWorkspace] ?? {};
16
+ homeEntry.hasTrustDialogAccepted = true;
17
+ projects[input.homeWorkspace] = homeEntry;
18
+ let seeded = 0;
19
+ let alreadyTrusted = 0;
20
+ for (const dir of input.accountDirs) {
21
+ const entry = projects[dir] ?? {};
22
+ if (entry.hasTrustDialogAccepted === true) {
23
+ alreadyTrusted += 1;
24
+ }
25
+ else {
26
+ entry.hasTrustDialogAccepted = true;
27
+ seeded += 1;
28
+ }
29
+ projects[dir] = entry;
30
+ }
31
+ // A deleted account cannot be spawned into, so its entry is left exactly as
32
+ // the CLI wrote it. Counting it separately keeps the install line's
33
+ // `seeded + alreadyTrusted === accounts` invariant meaningful.
34
+ //
35
+ // Direct children only. The CLI records an entry for ANY directory a session
36
+ // ran in, so an account's own subdirectory (a `sites/<project>` tree, say)
37
+ // also sits under this prefix; counting those as stale accounts would inflate
38
+ // the field every time somebody worked inside an account.
39
+ const onDisk = new Set(input.accountDirs);
40
+ const prefix = `${input.accountsRoot}/`;
41
+ const stale = Object.keys(projects).filter((k) => k.startsWith(prefix) && !k.slice(prefix.length).includes("/") && !onDisk.has(k)).length;
42
+ state.projects = projects;
43
+ return { state, accounts: input.accountDirs.length, seeded, alreadyTrusted, stale };
44
+ }
45
+ /** `phase` distinguishes the two passes: `deploy` seeds an upgrade's existing
46
+ * accounts, `post-account` seeds the directory a fresh install just created.
47
+ * Without it the two lines are byte-identical and the operator cannot tell
48
+ * which pass did the work. */
49
+ export function formatTrustSeedLine(file, home, r, error, phase) {
50
+ return (` [install] op=claude-trust-seed phase=${phase} file=${file} home=${home} accounts=${r.accounts}` +
51
+ ` seeded=${r.seeded} alreadyTrusted=${r.alreadyTrusted} stale=${r.stale} error=${error}`);
52
+ }
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import { parseSwVers, isSupportedMacosVersion } from "./macos-version.js";
27
27
  import { decideChromiumAction, isSnapConfinedPath } from "./snap-chromium.js";
28
28
  import { classifyPortHolder } from "./preflight-port-classifier.js";
29
29
  import { memoryControllerMissing, cmdlineAlreadyPatched, patchCmdline } from "./cgroup-memory-controller.js";
30
+ import { seedClaudeTrust, formatTrustSeedLine } from "./claude-trust-seed.js";
30
31
  import { parsePluginList, computeInstallActions, parseExternalPlugins, findUnregisteredResyncs, discoverMarketplaceCandidateDirs, } from "./lib/plugin-install.js";
31
32
  import { findPremiumMcpDirs } from "./lib/premium-mcp-discover.js";
32
33
  import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, pickWebsockifyBind, formatWebsockifyBindFile, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
@@ -2488,6 +2489,67 @@ function runInstallInvariantCheck(usersFile, accountsDir) {
2488
2489
  }
2489
2490
  console.log(` ${TAG} check complete divergences=${divergences}`);
2490
2491
  }
2492
+ // pre-populate the brand `.claude.json` so the manager's PTY-spawned
2493
+ // `claude --verbose --remote-control` boots straight into the session URL
2494
+ // without stopping at the first-run theme picker or the workspace-trust
2495
+ // dialog. Both prompts ate the manager's 30 s url-capture window on
2496
+ // 0.1.6 / 0.1.8 installs (Pi 192.168.88.16 verified). The markers required:
2497
+ // - hasCompletedOnboarding: true (suppresses theme picker)
2498
+ // - projects[<dir>].hasTrustDialogAccepted: true, for $HOME AND for every
2499
+ // account directory (Task 2720). Claude Code scopes folder trust to the
2500
+ // git repository root, and provision-account-dir.sh gives each account
2501
+ // directory its own repo, so $HOME's trust does not reach them. Seeding
2502
+ // $HOME alone left every spawn halted on the trust dialog.
2503
+ // Existing fields (firstStartTime, oauthAccount, migrationVersion, …) are
2504
+ // preserved so a brand re-install does not clobber state written by claude
2505
+ // during the previous run.
2506
+ //
2507
+ // Called TWICE, and both calls are load-bearing. `deploy` runs inside
2508
+ // deployPayload, which is where an upgrade's already-existing account
2509
+ // directories are seeded; `post-account` runs after setupAccount(), which is
2510
+ // the only point a FRESH install has an account directory on disk at all.
2511
+ // Running deploy alone would leave every fresh install untrusted and hang its
2512
+ // first session. The transform is idempotent, so the second pass rewrites
2513
+ // nothing the first already did (`seeded=0` when there was nothing new).
2514
+ function primeClaudeState(phase) {
2515
+ const persistentDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
2516
+ const persistentClaudeDir = join(persistentDir, ".claude");
2517
+ mkdirSync(persistentClaudeDir, { recursive: true });
2518
+ const claudeStateFile = join(persistentClaudeDir, ".claude.json");
2519
+ const homeWorkspace = process.env.HOME ?? "/root";
2520
+ let claudeState = {};
2521
+ if (existsSync(claudeStateFile)) {
2522
+ try {
2523
+ claudeState = JSON.parse(readFileSync(claudeStateFile, "utf-8"));
2524
+ }
2525
+ catch {
2526
+ claudeState = {};
2527
+ }
2528
+ }
2529
+ // An absent accounts directory is a first install, not an error; a listing
2530
+ // that throws is reported on the line rather than aborting the install.
2531
+ const accountsRoot = join(INSTALL_DIR, "data/accounts");
2532
+ let accountDirs = [];
2533
+ let seedError = "none";
2534
+ try {
2535
+ if (existsSync(accountsRoot)) {
2536
+ accountDirs = readdirSync(accountsRoot, { withFileTypes: true })
2537
+ .filter((e) => e.isDirectory())
2538
+ .map((e) => join(accountsRoot, e.name));
2539
+ }
2540
+ }
2541
+ catch (err) {
2542
+ seedError = err instanceof Error ? err.message : String(err);
2543
+ }
2544
+ const seedResult = seedClaudeTrust({
2545
+ state: claudeState,
2546
+ homeWorkspace,
2547
+ accountsRoot,
2548
+ accountDirs,
2549
+ });
2550
+ writeFileSync(claudeStateFile, JSON.stringify(seedResult.state, null, 2) + "\n", { mode: 0o600 });
2551
+ console.log(formatTrustSeedLine(claudeStateFile, homeWorkspace, seedResult, seedError, phase));
2552
+ }
2491
2553
  function deployPayload() {
2492
2554
  log("8", TOTAL, `Deploying ${BRAND.productName}...`);
2493
2555
  if (!existsSync(PAYLOAD_DIR)) {
@@ -2511,39 +2573,8 @@ function deployPayload() {
2511
2573
  // `claude` SDK subprocess and admin server read from there via the
2512
2574
  // Environment=CLAUDE_CONFIG_DIR= block in port-resolution.ts. Fresh
2513
2575
  // installs prompt the operator to run `claude /login`.
2514
- const persistentClaudeDir = join(persistentDir, ".claude");
2515
- mkdirSync(persistentClaudeDir, { recursive: true });
2516
- // pre-populate the brand `.claude.json` so the manager's
2517
- // PTY-spawned `claude --verbose --remote-control` boots straight into the
2518
- // session URL without stopping at the first-run theme picker or the
2519
- // workspace-trust dialog. Both prompts ate the manager's 30 s url-capture
2520
- // window on 0.1.6 / 0.1.8 installs (Pi 192.168.88.16 verified). The two
2521
- // markers required are:
2522
- // - hasCompletedOnboarding: true (suppresses theme picker)
2523
- // - projects[$HOME].hasTrustDialogAccepted: true
2524
- // Existing fields (firstStartTime, oauthAccount, migrationVersion, …) are
2525
- // preserved so a brand re-install does not clobber state written by claude
2526
- // during the previous run.
2527
- const claudeStateFile = join(persistentClaudeDir, ".claude.json");
2528
- const homeWorkspace = process.env.HOME ?? "/root";
2529
- let claudeState = {};
2530
- if (existsSync(claudeStateFile)) {
2531
- try {
2532
- claudeState = JSON.parse(readFileSync(claudeStateFile, "utf-8"));
2533
- }
2534
- catch {
2535
- claudeState = {};
2536
- }
2537
- }
2538
- claudeState.hasCompletedOnboarding = true;
2539
- claudeState.theme = claudeState.theme ?? "auto";
2540
- const projects = (claudeState.projects ?? {});
2541
- const homeEntry = projects[homeWorkspace] ?? {};
2542
- homeEntry.hasTrustDialogAccepted = true;
2543
- projects[homeWorkspace] = homeEntry;
2544
- claudeState.projects = projects;
2545
- writeFileSync(claudeStateFile, JSON.stringify(claudeState, null, 2) + "\n", { mode: 0o600 });
2546
- console.log(` [install] claude-state primed: ${claudeStateFile} hasCompletedOnboarding=true trustedWorkspace=${homeWorkspace}`);
2576
+ // primeClaudeState creates that directory, so there is no separate mkdir here.
2577
+ primeClaudeState("deploy");
2547
2578
  // Brand isolation: installer does not read ~/.maxy/, ~/.cloudflared/, or
2548
2579
  // ~/.cloudflare/ on non-default brands. These are peer-brand or
2549
2580
  // shared-singleton paths.
@@ -5458,6 +5489,10 @@ try {
5458
5489
  registerLocalAndExternalPlugins(); // install-time plugin registration
5459
5490
  setupVncViewer();
5460
5491
  setupAccount();
5492
+ // Task 2720 — the account directory only exists from here on a fresh
5493
+ // install, and provision-account-dir.sh has just made it a git repo root,
5494
+ // so this is the pass that trusts it. Idempotent on the upgrade path.
5495
+ primeClaudeState("post-account");
5461
5496
  if (ENTITLEMENT_PAYLOAD) {
5462
5497
  writeEntitlementFromFlag(ENTITLEMENT_PAYLOAD);
5463
5498
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.605",
3
+ "version": "0.1.606",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -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:6af2c17c66037f12d4a3172828a7db1fef57c3a15e9be8264072e9ae2ae5b362
4
+ content-hash: sha256:4cf85978124889a43767660591634992d687b20aa692836be272c878c4505481
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -325,6 +325,8 @@ If you suspect background processes are piling up, run `grep '\[reaper\]' ~/.{br
325
325
 
326
326
  Every install seeds `permissions.allow:[]` plus `defaultMode:"bypassPermissions"` into both the brand-scoped settings file (`~/.{brand}/.claude/settings.json`) and every account-scoped one (`<install>/data/accounts/<id>/.claude/settings.json`). `bypassPermissions` alone stops Claude Code from sending tool calls to its remote auto-classifier (it skips all permission checks), which would otherwise surface a permission prompt in the chat that an unattended session never answers. The `allow` array stays empty and **must never contain `"*"`**: Claude Code ≥ 2.1.167 rejects `"*"` as an allow token and renders a blocking Settings Warning that, under `--remote-control`, stops every `/rc-spawn` from binding. What each subagent is allowed to use is still controlled by the `tools:` line in its agent file, not by a top-level allowlist. To verify after an install: `cat ~/.{brand}/.claude/settings.json | jq '.permissions'` (expect `allow: []`). To repair an install seeded before this change: `bash <install>/platform/scripts/backfill-bypass-permissions.sh ~/.{brand}`.
327
327
 
328
+ **Workspace trust.** Separate from permissions, and its own way to stop a spawn dead. Claude Code refuses to start in a directory it does not trust, and it scopes that trust to the **git repository root** of the working directory rather than to any trusted ancestor. Every account directory is its own repository (`provision-account-dir.sh` runs `git init` there so Claude Code finds the account's own `.claude/` hook set), so trusting the home directory does not reach them. Every install therefore seeds `projects[<dir>].hasTrustDialogAccepted: true` into `~/.{brand}/.claude/.claude.json` for the home directory **and for every directory under `<install>/data/accounts`**, preserving all other fields the CLI wrote. Without it the PTY-spawned session halts on "Quick safety check: Is this a project you created or one you trust?" and never binds, which surfaces as a session that starts and answers nothing, with no error in the interface. The seeding runs twice per install and both passes matter: the `deploy` pass trusts the accounts an upgrade already has, and the `post-account` pass, which runs straight after account setup, trusts the account a brand-new install has only just created. The install line is `[install] op=claude-trust-seed phase=<deploy|post-account> file=… accounts=<N> seeded=<n> alreadyTrusted=<n> stale=<n> error=<none|msg>`; `seeded + alreadyTrusted` equals `accounts`, and a re-run reads `seeded=0`. Because an account added *after* an install has an untrusted repository root that no event announces, the session manager reconciles the two every hour and logs `[claude-trust-census] accounts=<N> trusted=<n> untrusted=<n> untrustedDirs=<csv|none> homeTrusted=<bool> onboarded=<bool>` — a non-zero `untrusted` names the directories whose next session will hang. To verify after an install: `node -e 'const j=require(process.env.HOME+"/.{brand}/.claude/.claude.json");const p=j.projects||{};for(const k of Object.keys(p))if(k.includes("/data/accounts/"))console.log(k,p[k].hasTrustDialogAccepted)'` (expect `true` for every account directory that exists).
329
+
328
330
  **`autoMode` trust block.** The same brand-scoped writer ([`permissions-seed.ts`](../../../packages/create-maxy-code/src/permissions-seed.ts), `AUTO_MODE_BLOCK`) also seeds the harness `autoMode` block. `bypassPermissions` skips the auto-mode classifier entirely, so the block is **belt-and-suspenders**: it only matters when a session is toggled into `auto` mode, where the classifier otherwise soft-denies the doctrine-sanctioned Cloudflare scoped-token mint ("permission/credential escalation") because the install's own Cloudflare infra is not in the classifier's default trusted environment. The block keeps `"$defaults"` first in `environment` and `allow` (so the built-in Data-Exfiltration / force-push / prod-deploy guards stay intact) and adds prose declaring `api.cloudflare.com` the install's own provider endpoint plus an allow for minting/persisting narrow per-scope tokens. It is seeded **only** into the brand/user settings (`~/.{brand}/.claude/settings.json`) — the classifier never reads `autoMode` from the shared project (account-scoped) settings, so `setup-account.sh` does not carry it. An auto-mode agent cannot seed this itself (the default `hard_deny` "Auto-Mode Bypass" blocks any agent editing `.claude/settings*.json`); installer seed is the only path. To verify after an install: `CLAUDE_CONFIG_DIR=~/.{brand}/.claude claude auto-mode config | jq '.environment, .allow'` (expect the Cloudflare entries, `$defaults` expanded) and `jq '.autoMode' ~/.{brand}/.claude/settings.json`.
329
331
 
330
332
  ---
@@ -4095,6 +4097,19 @@ which fails `prebuild` if any emoji codepoint appears in a `.ts`/`.tsx` file
4095
4097
  under `app/` (tests excluded). Inline an icon at the surrounding text size and
4096
4098
  colour (`size={14}` matches body text; Lucide inherits `currentColor`).
4097
4099
 
4100
+ **A long message shows its first 8 lines.** On `/chat`, the WhatsApp and
4101
+ Telegram readers and the public visitor chat, a message body taller than 8 lines
4102
+ is cut at 8 and carries a "Show more…" control that reveals the rest; "Show
4103
+ less" puts it back. The control appears only when there is genuinely more to
4104
+ see, so a short message looks exactly as it did before. Expanding one message
4105
+ expands only that message, and scrolling back through older messages does not
4106
+ carry an expansion onto a different one. Two things are never cut: an error the
4107
+ agent reports, and the map on a shared location. The reader's search also opens
4108
+ any message whose match would otherwise sit in the hidden part, so a search
4109
+ never finds something it then hides. See
4110
+ [`.docs/message-body-clamp.md`](../../../.docs/message-body-clamp.md) for the
4111
+ render sites and the census that watches it.
4112
+
4098
4113
  ## Admin Hono routes
4099
4114
 
4100
4115
  Every admin sub-app is mounted by
@@ -9,6 +9,11 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-14 (0.1.606)
13
+
14
+ - Conversations start after an install again. The installer marked only your home folder as trusted, and the assistant now asks about the folder each account actually runs from, so a session would stop at a trust prompt nobody could answer.
15
+ - A long chat message is capped at eight lines with a control to show the rest, instead of filling the pane.
16
+
12
17
  ## 2026-08-14 (0.1.605)
13
18
 
14
19
  - Five more routines now check whether there is anything to do before waking the assistant. Mail triage, appointment reminders, the operational and finance reconciles, and the two health reports all previously ran on the clock and did the deciding after starting up.
@@ -37,6 +37,19 @@ which fails `prebuild` if any emoji codepoint appears in a `.ts`/`.tsx` file
37
37
  under `app/` (tests excluded). Inline an icon at the surrounding text size and
38
38
  colour (`size={14}` matches body text; Lucide inherits `currentColor`).
39
39
 
40
+ **A long message shows its first 8 lines.** On `/chat`, the WhatsApp and
41
+ Telegram readers and the public visitor chat, a message body taller than 8 lines
42
+ is cut at 8 and carries a "Show more…" control that reveals the rest; "Show
43
+ less" puts it back. The control appears only when there is genuinely more to
44
+ see, so a short message looks exactly as it did before. Expanding one message
45
+ expands only that message, and scrolling back through older messages does not
46
+ carry an expansion onto a different one. Two things are never cut: an error the
47
+ agent reports, and the map on a shared location. The reader's search also opens
48
+ any message whose match would otherwise sit in the hidden part, so a search
49
+ never finds something it then hides. See
50
+ [`.docs/message-body-clamp.md`](../../../.docs/message-body-clamp.md) for the
51
+ render sites and the census that watches it.
52
+
40
53
  ## Admin Hono routes
41
54
 
42
55
  Every admin sub-app is mounted by
@@ -198,4 +198,6 @@ If you suspect background processes are piling up, run `grep '\[reaper\]' ~/.{br
198
198
 
199
199
  Every install seeds `permissions.allow:[]` plus `defaultMode:"bypassPermissions"` into both the brand-scoped settings file (`~/.{brand}/.claude/settings.json`) and every account-scoped one (`<install>/data/accounts/<id>/.claude/settings.json`). `bypassPermissions` alone stops Claude Code from sending tool calls to its remote auto-classifier (it skips all permission checks), which would otherwise surface a permission prompt in the chat that an unattended session never answers. The `allow` array stays empty and **must never contain `"*"`**: Claude Code ≥ 2.1.167 rejects `"*"` as an allow token and renders a blocking Settings Warning that, under `--remote-control`, stops every `/rc-spawn` from binding. What each subagent is allowed to use is still controlled by the `tools:` line in its agent file, not by a top-level allowlist. To verify after an install: `cat ~/.{brand}/.claude/settings.json | jq '.permissions'` (expect `allow: []`). To repair an install seeded before this change: `bash <install>/platform/scripts/backfill-bypass-permissions.sh ~/.{brand}`.
200
200
 
201
+ **Workspace trust.** Separate from permissions, and its own way to stop a spawn dead. Claude Code refuses to start in a directory it does not trust, and it scopes that trust to the **git repository root** of the working directory rather than to any trusted ancestor. Every account directory is its own repository (`provision-account-dir.sh` runs `git init` there so Claude Code finds the account's own `.claude/` hook set), so trusting the home directory does not reach them. Every install therefore seeds `projects[<dir>].hasTrustDialogAccepted: true` into `~/.{brand}/.claude/.claude.json` for the home directory **and for every directory under `<install>/data/accounts`**, preserving all other fields the CLI wrote. Without it the PTY-spawned session halts on "Quick safety check: Is this a project you created or one you trust?" and never binds, which surfaces as a session that starts and answers nothing, with no error in the interface. The seeding runs twice per install and both passes matter: the `deploy` pass trusts the accounts an upgrade already has, and the `post-account` pass, which runs straight after account setup, trusts the account a brand-new install has only just created. The install line is `[install] op=claude-trust-seed phase=<deploy|post-account> file=… accounts=<N> seeded=<n> alreadyTrusted=<n> stale=<n> error=<none|msg>`; `seeded + alreadyTrusted` equals `accounts`, and a re-run reads `seeded=0`. Because an account added *after* an install has an untrusted repository root that no event announces, the session manager reconciles the two every hour and logs `[claude-trust-census] accounts=<N> trusted=<n> untrusted=<n> untrustedDirs=<csv|none> homeTrusted=<bool> onboarded=<bool>` — a non-zero `untrusted` names the directories whose next session will hang. To verify after an install: `node -e 'const j=require(process.env.HOME+"/.{brand}/.claude/.claude.json");const p=j.projects||{};for(const k of Object.keys(p))if(k.includes("/data/accounts/"))console.log(k,p[k].hasTrustDialogAccepted)'` (expect `true` for every account directory that exists).
202
+
201
203
  **`autoMode` trust block.** The same brand-scoped writer ([`permissions-seed.ts`](../../../packages/create-maxy-code/src/permissions-seed.ts), `AUTO_MODE_BLOCK`) also seeds the harness `autoMode` block. `bypassPermissions` skips the auto-mode classifier entirely, so the block is **belt-and-suspenders**: it only matters when a session is toggled into `auto` mode, where the classifier otherwise soft-denies the doctrine-sanctioned Cloudflare scoped-token mint ("permission/credential escalation") because the install's own Cloudflare infra is not in the classifier's default trusted environment. The block keeps `"$defaults"` first in `environment` and `allow` (so the built-in Data-Exfiltration / force-push / prod-deploy guards stay intact) and adds prose declaring `api.cloudflare.com` the install's own provider endpoint plus an allow for minting/persisting narrow per-scope tokens. It is seeded **only** into the brand/user settings (`~/.{brand}/.claude/settings.json`) — the classifier never reads `autoMode` from the shared project (account-scoped) settings, so `setup-account.sh` does not carry it. An auto-mode agent cannot seed this itself (the default `hard_deny` "Auto-Mode Bypass" blocks any agent editing `.claude/settings*.json`); installer seed is the only path. To verify after an install: `CLAUDE_CONFIG_DIR=~/.{brand}/.claude claude auto-mode config | jq '.environment, .allow'` (expect the Cloudflare entries, `$defaults` expanded) and `jq '.autoMode' ~/.{brand}/.claude/settings.json`.
@@ -0,0 +1,34 @@
1
+ export type Logger = (line: string) => void;
2
+ export interface TrustFacts {
3
+ accounts: number;
4
+ trusted: number;
5
+ untrusted: number;
6
+ untrustedDirs: string[];
7
+ homeTrusted: boolean;
8
+ onboarded: boolean;
9
+ }
10
+ /** Pure. An account directory with no entry counts as untrusted, not absent. */
11
+ export declare function collectTrustFacts(input: {
12
+ state: Record<string, unknown>;
13
+ homeWorkspace: string;
14
+ accountDirs: string[];
15
+ }): TrustFacts;
16
+ /** Pure. */
17
+ export declare function formatTrustCensus(f: TrustFacts): string;
18
+ export interface TrustCensusDeps {
19
+ claudeJsonPath: string;
20
+ accountsDir: string;
21
+ homeWorkspace: string;
22
+ logger: Logger;
23
+ intervalMs: number;
24
+ /** Injected in tests so the collector runs without a filesystem. */
25
+ readState?: () => Record<string, unknown>;
26
+ listAccountDirs?: () => string[];
27
+ }
28
+ export interface TrustCensus {
29
+ tickOnce(): void;
30
+ start(): void;
31
+ stop(): void;
32
+ }
33
+ export declare function createClaudeTrustCensus(deps: TrustCensusDeps): TrustCensus;
34
+ //# sourceMappingURL=claude-trust-census.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-trust-census.d.ts","sourceRoot":"","sources":["../src/claude-trust-census.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAE3C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,GAAG,UAAU,CAWb;AAED,YAAY;AACZ,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAMvD;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC,eAAe,CAAC,EAAE,MAAM,MAAM,EAAE,CAAA;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,IAAI,IAAI,CAAA;IAChB,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CA6C1E"}
@@ -0,0 +1,83 @@
1
+ // Task 2720 — standing reconciliation of account directories against the
2
+ // workspace-trust entries in the brand's `.claude.json`.
3
+ //
4
+ // Claude Code scopes folder trust to the git repository root, and
5
+ // provision-account-dir.sh gives every account directory its own repo, so a
6
+ // directory with no `hasTrustDialogAccepted: true` entry halts the spawned
7
+ // child on the folder-trust dialog before it binds.
8
+ //
9
+ // Two of that defect's failure modes emit no event and do not reproduce on
10
+ // demand: an installer enumeration that skipped a directory, and an account
11
+ // created after the install (a fresh repo root with no entry). Neither
12
+ // produces a log line until somebody tries to spawn a session there, and by
13
+ // then the symptom is a 60 s bind timeout that `/rc-spawn`'s 200 turns into
14
+ // silence. Only a periodic reconcile of expected against actual surfaces them.
15
+ //
16
+ // Shape mirrors the other standing censuses in this service (a pure collector,
17
+ // a pure formatter, and a start/stop factory whose setInterval is unref'd):
18
+ // see `role-census.ts`.
19
+ import { readFileSync, readdirSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ const TAG = '[claude-trust-census]';
22
+ /** Pure. An account directory with no entry counts as untrusted, not absent. */
23
+ export function collectTrustFacts(input) {
24
+ const projects = (input.state.projects ?? {});
25
+ const untrustedDirs = input.accountDirs.filter((d) => projects[d]?.hasTrustDialogAccepted !== true);
26
+ return {
27
+ accounts: input.accountDirs.length,
28
+ trusted: input.accountDirs.length - untrustedDirs.length,
29
+ untrusted: untrustedDirs.length,
30
+ untrustedDirs,
31
+ homeTrusted: projects[input.homeWorkspace]?.hasTrustDialogAccepted === true,
32
+ onboarded: input.state.hasCompletedOnboarding === true,
33
+ };
34
+ }
35
+ /** Pure. */
36
+ export function formatTrustCensus(f) {
37
+ const dirs = f.untrustedDirs.length === 0 ? 'none' : f.untrustedDirs.join(',');
38
+ return (`${TAG} accounts=${f.accounts} trusted=${f.trusted} untrusted=${f.untrusted}` +
39
+ ` untrustedDirs=${dirs} homeTrusted=${f.homeTrusted} onboarded=${f.onboarded}`);
40
+ }
41
+ export function createClaudeTrustCensus(deps) {
42
+ let timer = null;
43
+ const readState = deps.readState ??
44
+ (() => JSON.parse(readFileSync(deps.claudeJsonPath, 'utf-8')));
45
+ const listAccountDirs = deps.listAccountDirs ??
46
+ (() => readdirSync(deps.accountsDir, { withFileTypes: true })
47
+ .filter((e) => e.isDirectory())
48
+ .map((e) => join(deps.accountsDir, e.name)));
49
+ function tickOnce() {
50
+ try {
51
+ const facts = collectTrustFacts({
52
+ state: readState(),
53
+ homeWorkspace: deps.homeWorkspace,
54
+ accountDirs: listAccountDirs(),
55
+ });
56
+ // Emitted on a clean result too. A quiet interval is itself the signal
57
+ // that the census is alive, so a missing line means the audit stopped
58
+ // rather than that every directory is trusted.
59
+ deps.logger(formatTrustCensus(facts));
60
+ }
61
+ catch (err) {
62
+ deps.logger(`${TAG} op=census-failed message=${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ }
65
+ return {
66
+ tickOnce,
67
+ start() {
68
+ if (timer)
69
+ return;
70
+ timer = setInterval(tickOnce, deps.intervalMs);
71
+ if (typeof timer.unref === 'function')
72
+ timer.unref();
73
+ deps.logger(`${TAG} op=census-started intervalMs=${deps.intervalMs}`);
74
+ },
75
+ stop() {
76
+ if (timer) {
77
+ clearInterval(timer);
78
+ timer = null;
79
+ }
80
+ },
81
+ };
82
+ }
83
+ //# sourceMappingURL=claude-trust-census.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-trust-census.js","sourceRoot":"","sources":["../src/claude-trust-census.ts"],"names":[],"mappings":"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"}
@@ -36,6 +36,7 @@ import { createPermissionModeTail } from './permission-mode-tail.js';
36
36
  import { createReaper } from './reaper.js';
37
37
  import { createReachabilityAudit } from './public-agent-reachability.js';
38
38
  import { createRoleCensus } from './role-census.js';
39
+ import { createClaudeTrustCensus } from './claude-trust-census.js';
39
40
  import { createPublicPromptSurfaceCensus } from './public-prompt-surface.js';
40
41
  import { createPublicKnowledgeAudit, readPublicAgentKnowledge } from './public-knowledge-audit.js';
41
42
  import { createPtyCensus } from './pty-census.js';
@@ -1264,6 +1265,18 @@ async function main() {
1264
1265
  intervalMs: 900_000,
1265
1266
  });
1266
1267
  quoteReconcile.start();
1268
+ // Task 2720 — hourly reconcile of account directories against their
1269
+ // workspace-trust entries in the brand's `.claude.json`. An account created
1270
+ // after the install is a fresh git repo root with no trust entry, and
1271
+ // nothing announces it: the first spawn there halts on the folder-trust
1272
+ // dialog and /rc-spawn's 200 hides the failure. Purely filesystem.
1273
+ createClaudeTrustCensus({
1274
+ claudeJsonPath: join(claudeConfigDirResolved, '.claude.json'),
1275
+ accountsDir: dirname(config.accountDir),
1276
+ homeWorkspace: process.env.HOME ?? '/root',
1277
+ logger: log,
1278
+ intervalMs: 3_600_000,
1279
+ }).start();
1267
1280
  // Task 183 — recorder JSONL tail. Posts [recorder-session-init] and
1268
1281
  // [recorder-turn] lines for every database-operator specialist spawn,
1269
1282
  // routed through the platform UI's /api/admin/log-ingest loopback