@rubytech/create-maxy-code 0.1.567 → 0.1.569

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 (120) hide show
  1. package/dist/__tests__/brand-agent-root.test.js +91 -0
  2. package/dist/brand-agent-root.js +58 -0
  3. package/dist/index.js +25 -28
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/PLUGIN.md +4 -2
  6. package/payload/platform/plugins/admin/hooks/__tests__/quote-path-write-gate.test.sh +152 -0
  7. package/payload/platform/plugins/admin/hooks/__tests__/quote-pdf-bash-gate.test.sh +95 -0
  8. package/payload/platform/plugins/admin/hooks/__tests__/quote-render-gate.test.sh +141 -45
  9. package/payload/platform/plugins/admin/hooks/__tests__/quote-render-pdf-conformance.test.sh +114 -0
  10. package/payload/platform/plugins/admin/hooks/quote-path-write-gate.sh +137 -0
  11. package/payload/platform/plugins/admin/hooks/quote-pdf-bash-gate.sh +70 -0
  12. package/payload/platform/plugins/admin/hooks/quote-render-gate.sh +105 -37
  13. package/payload/platform/plugins/admin/hooks/quote-render-pdf-conformance.sh +178 -0
  14. package/payload/platform/plugins/admin/mcp/dist/index.js +7 -1
  15. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.d.ts +10 -1
  17. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.d.ts.map +1 -1
  18. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.js +52 -1
  19. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.js.map +1 -1
  20. package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.d.ts +9 -0
  21. package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.d.ts.map +1 -1
  22. package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.js +1 -1
  23. package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.js.map +1 -1
  24. package/payload/platform/plugins/admin/skills/capabilities-here/SKILL.md +1 -1
  25. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +44 -1
  26. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +15 -0
  27. package/payload/platform/plugins/docs/references/channel-wake-and-prompt.md +43 -0
  28. package/payload/platform/scripts/__tests__/mailbox-inject-registered.test.sh +5 -5
  29. package/payload/platform/scripts/__tests__/preference-hooks-registered.test.sh +3 -1
  30. package/payload/platform/scripts/__tests__/public-surface-registered.test.sh +5 -5
  31. package/payload/platform/scripts/__tests__/quote-ownership-contract.test.sh +86 -0
  32. package/payload/platform/scripts/lib/__tests__/account-settings-reconcile.test.sh +73 -0
  33. package/payload/platform/scripts/lib/account-settings-reconcile.sh +88 -0
  34. package/payload/platform/scripts/lib/provision-account-dir.sh +11 -146
  35. package/payload/platform/scripts/setup-account.sh +13 -0
  36. package/payload/platform/services/claude-session-manager/dist/account-agent-registry.d.ts +39 -0
  37. package/payload/platform/services/claude-session-manager/dist/account-agent-registry.d.ts.map +1 -0
  38. package/payload/platform/services/claude-session-manager/dist/account-agent-registry.js +223 -0
  39. package/payload/platform/services/claude-session-manager/dist/account-agent-registry.js.map +1 -0
  40. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +5 -0
  41. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +5 -5
  43. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +11 -1
  45. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/http-server.js +58 -12
  47. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/index.js +112 -13
  49. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +6 -1
  51. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
  52. package/payload/platform/services/claude-session-manager/dist/pty-census.js +6 -3
  53. package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
  54. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +21 -8
  55. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  56. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +38 -15
  57. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  58. package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.d.ts +42 -0
  59. package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.d.ts.map +1 -0
  60. package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js +215 -0
  61. package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js.map +1 -0
  62. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +2 -0
  63. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  64. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +4 -1
  65. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  66. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts +3 -0
  67. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
  68. package/payload/platform/services/claude-session-manager/dist/reaper.js +2 -1
  69. package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
  70. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.d.ts +2 -0
  71. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.d.ts.map +1 -1
  72. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.js +5 -2
  73. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.js.map +1 -1
  74. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +2 -0
  75. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -1
  76. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +5 -3
  77. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -1
  78. package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.d.ts +108 -0
  79. package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.d.ts.map +1 -0
  80. package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.js +299 -0
  81. package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.js.map +1 -0
  82. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +26 -16
  83. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
  84. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +84 -31
  85. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
  86. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts +7 -0
  87. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts.map +1 -1
  88. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js +6 -0
  89. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js.map +1 -1
  90. package/payload/platform/services/telegram-channel/dist/notification.d.ts +10 -2
  91. package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
  92. package/payload/platform/services/telegram-channel/dist/notification.js +15 -3
  93. package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
  94. package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -1
  95. package/payload/platform/services/telegram-channel/dist/server.js +35 -3
  96. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
  97. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts +10 -0
  98. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts.map +1 -1
  99. package/payload/platform/services/telegram-channel/dist/turn-follow.js +38 -1
  100. package/payload/platform/services/telegram-channel/dist/turn-follow.js.map +1 -1
  101. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  102. package/payload/platform/services/webchat-channel/dist/server.js +31 -2
  103. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  104. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts +10 -0
  105. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
  106. package/payload/platform/services/webchat-channel/dist/turn-follow.js +38 -1
  107. package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
  108. package/payload/platform/services/whatsapp-channel/dist/instructions.d.ts +9 -0
  109. package/payload/platform/services/whatsapp-channel/dist/instructions.d.ts.map +1 -0
  110. package/payload/platform/services/whatsapp-channel/dist/instructions.js +22 -0
  111. package/payload/platform/services/whatsapp-channel/dist/instructions.js.map +1 -0
  112. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +3 -1
  113. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  114. package/payload/platform/services/whatsapp-channel/dist/notification.js +10 -2
  115. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  116. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  117. package/payload/platform/services/whatsapp-channel/dist/server.js +26 -2
  118. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  119. package/payload/platform/templates/account-settings.json +153 -0
  120. package/payload/server/server.js +254 -96
@@ -0,0 +1,299 @@
1
+ // Task 2469 — a specialist that CAN be reached is not a specialist that WAS.
2
+ //
3
+ // The registry reconcile in account-agent-registry.ts answers the first
4
+ // question and emits `op=agent-registry-resolution`. It was green on the
5
+ // account that filed this, and would have stayed green through the whole
6
+ // incident, because every clerk was reachable and none was reached: four
7
+ // authored clerks owned the works orders, invoices and client emails while
8
+ // content-producer produced all of them. The two questions are deliberately
9
+ // separate signals, because one cannot answer the other.
10
+ //
11
+ // Keyed on the written path rather than on the dispatch prose. A dispatch
12
+ // description is the model's own account of what it is doing, and the incident
13
+ // this closes had twelve descriptions that each read as reasonable. A path is
14
+ // a fact, and it is the same fact a write gate enforces on, so enforcement and
15
+ // audit cannot drift apart.
16
+ //
17
+ // The input is `<projectDir>/<sessionId>/subagents/agent-<agentId>.jsonl` with
18
+ // its sibling `.meta.json`. That directory is the ONLY surface that ties a
19
+ // write to a specialist: the parent transcript records every record as
20
+ // `isSidechain: false`, carries none of the subagent's tool calls, and its
21
+ // `toolUseResult.toolStats` gives aggregate counts with no paths.
22
+ //
23
+ // Shape mirrors public-surface-audit.ts: PURE parsers, selector and formatters
24
+ // plus an impure factory whose setInterval is unref'd and whose readers are
25
+ // injected.
26
+ import { closeSync, openSync, readSync } from 'node:fs';
27
+ import { StringDecoder } from 'node:string_decoder';
28
+ const TAG = '[specialist-preference]';
29
+ /** Lines of a file, read in bounded chunks rather than into one string.
30
+ *
31
+ * One session's `subagents/` directory measured 473 MB, so reading a
32
+ * transcript with `readFileSync` would put the whole thing on the heap to look
33
+ * at one field per line. A `StringDecoder` carries partial multi-byte
34
+ * characters across chunk boundaries: splitting one would corrupt the line,
35
+ * `JSON.parse` would reject it, and the write it carried would vanish from
36
+ * the audit silently. */
37
+ export function* readLinesSync(path, chunkBytes = 1 << 20) {
38
+ const fd = openSync(path, 'r');
39
+ try {
40
+ const buf = Buffer.allocUnsafe(chunkBytes);
41
+ const decoder = new StringDecoder('utf8');
42
+ let carry = '';
43
+ for (;;) {
44
+ const n = readSync(fd, buf, 0, chunkBytes, null);
45
+ if (n === 0)
46
+ break;
47
+ const parts = (carry + decoder.write(buf.subarray(0, n))).split('\n');
48
+ carry = parts.pop() ?? '';
49
+ for (const p of parts)
50
+ if (p.length > 0)
51
+ yield p;
52
+ }
53
+ carry += decoder.end();
54
+ if (carry.length > 0)
55
+ yield carry;
56
+ }
57
+ finally {
58
+ closeSync(fd);
59
+ }
60
+ }
61
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
62
+ /** Artefact directory to the specialist that owns it.
63
+ *
64
+ * Every entry is a directory measured in the live job tree: `quote` (17 jobs),
65
+ * `Quotations` (5), `orders` (8), `invoices` (1). There is no valuation
66
+ * directory anywhere in that tree, so valuation-clerk deliberately has no
67
+ * entry — a guessed owner produces misroute lines nobody can act on.
68
+ *
69
+ * Two exclusions are deliberate. `pricing/` holds the internal priced
70
+ * breakdown rather than a client-facing artefact. `Suppliers Invoices` is the
71
+ * supplier's invoice to the business, not the client invoice the
72
+ * invoice-clerk issues, and the match is exact-segment so it cannot be swept
73
+ * in by the `invoices` entry. */
74
+ export const OWNED_PATHS = [
75
+ { segment: 'quote', owner: 'sitedesk:quoter' },
76
+ { segment: 'Quotations', owner: 'sitedesk:quoter' },
77
+ { segment: 'orders', owner: 'order-clerk' },
78
+ { segment: 'invoices', owner: 'invoice-clerk' },
79
+ ];
80
+ /** The specialist that owns a written path, or null when the path is not an
81
+ * owned artefact. Anchored on `jobs/<jobId>/<segment>/`, so a same-named
82
+ * directory elsewhere in the account is not swept in and a file sitting
83
+ * directly in a job directory belongs to nobody in particular. */
84
+ export function ownerForPath(filePath) {
85
+ const seg = filePath.split('/');
86
+ for (let i = 0; i + 3 < seg.length; i++) {
87
+ if (seg[i] !== 'jobs')
88
+ continue;
89
+ const bucket = seg[i + 2];
90
+ const hit = OWNED_PATHS.find((o) => o.segment === bucket);
91
+ if (hit)
92
+ return hit.owner;
93
+ }
94
+ return null;
95
+ }
96
+ /** `agentType` from a subagent's sibling meta file, or null when the file is
97
+ * unreadable, unparseable, or carries no type. Null is never read as
98
+ * compliance: see `auditTranscript`. */
99
+ export function parseAgentType(metaJson) {
100
+ if (metaJson === null)
101
+ return null;
102
+ try {
103
+ const d = JSON.parse(metaJson);
104
+ const t = d?.agentType;
105
+ return typeof t === 'string' && t.length > 0 ? t : null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /** Written paths in one transcript line. A line that will not parse yields
112
+ * nothing rather than throwing: the corpus carries partial trailing writes,
113
+ * and one bad line must not abort the transcript it sits in. */
114
+ export function selectWrites(transcriptLine) {
115
+ let d;
116
+ try {
117
+ d = JSON.parse(transcriptLine);
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ const content = d?.message?.content;
123
+ if (!Array.isArray(content))
124
+ return [];
125
+ const out = [];
126
+ for (const b of content) {
127
+ if (!b || typeof b !== 'object')
128
+ continue;
129
+ const blk = b;
130
+ if (blk.type !== 'tool_use')
131
+ continue;
132
+ if (typeof blk.name !== 'string' || !WRITE_TOOLS.has(blk.name))
133
+ continue;
134
+ const fp = blk.input?.file_path;
135
+ if (typeof fp === 'string' && fp.length > 0)
136
+ out.push(fp);
137
+ }
138
+ return out;
139
+ }
140
+ /** One subagent run.
141
+ *
142
+ * `agentType` null means the meta file was missing or unreadable. Its
143
+ * owned-path writes are counted as unattributed rather than as compliant:
144
+ * "we could not tell who wrote this" is not "the right specialist wrote it",
145
+ * and folding the two together is how a broken reader reads as a clean
146
+ * account. */
147
+ export function auditTranscript(p) {
148
+ let ownedPathWrites = 0;
149
+ let unattributed = 0;
150
+ const misroutes = [];
151
+ for (const line of p.lines) {
152
+ for (const path of selectWrites(line)) {
153
+ const owner = ownerForPath(path);
154
+ if (owner === null)
155
+ continue;
156
+ ownedPathWrites++;
157
+ if (p.agentType === null) {
158
+ unattributed++;
159
+ continue;
160
+ }
161
+ if (p.agentType !== owner) {
162
+ misroutes.push({ session: p.session, agent: p.agentType, owner, path });
163
+ }
164
+ }
165
+ }
166
+ return { ownedPathWrites, unattributed, misroutes };
167
+ }
168
+ /** The every-tick line. Emitted including zero, so a run that found nothing is
169
+ * distinguishable from a run that did not happen. */
170
+ export function formatPreferenceSummary(p) {
171
+ return `${TAG} op=audit sessionsScanned=${p.sessionsScanned}` +
172
+ ` transcriptsScanned=${p.transcriptsScanned}` +
173
+ ` ownedPathWrites=${p.ownedPathWrites}` +
174
+ ` misrouted=${p.misrouted} unattributed=${p.unattributed}`;
175
+ }
176
+ /** One line per misroute, naming the subagent, the owner it displaced and the
177
+ * path. The displaced owner is the actionable half: "content-producer wrote
178
+ * this" says less than "content-producer wrote this instead of the quoter".
179
+ *
180
+ * The path is JSON-quoted because these paths contain spaces — the document
181
+ * that prompted this task is `Q 1879 - Woolgrove School - Yellow Class
182
+ * Kitchenette.html` — and a bare value would split into four tokens under any
183
+ * `key=value` reader, including `grep -o 'path=\S*'`. */
184
+ export function formatMisroute(p) {
185
+ return `${TAG} op=misroute session=${p.session} agent=${p.agent}` +
186
+ ` owner=${p.owner} path=${JSON.stringify(p.path)}`;
187
+ }
188
+ export function createSpecialistPreferenceAudit(deps) {
189
+ /** jsonl path -> the result of reading it at `mtimeMs`.
190
+ *
191
+ * The emitted counts are a STATE — the totals across every transcript
192
+ * currently known — not a per-tick delta. A transcript still being appended
193
+ * to is re-read when its mtime moves and its entry is REPLACED, so its
194
+ * earlier writes are counted once, not once per tick. A delta counter over
195
+ * a growing file would inflate every total it reported. */
196
+ const seen = new Map();
197
+ /** Misroutes already named, so a steady state does not re-log every tick. */
198
+ const logged = new Set();
199
+ let timer = null;
200
+ function tickOnce() {
201
+ let sessionsScanned = 0;
202
+ const live = new Set();
203
+ for (const session of deps.listSessions()) {
204
+ sessionsScanned++;
205
+ let transcripts;
206
+ try {
207
+ transcripts = deps.listTranscripts(session);
208
+ }
209
+ catch (err) {
210
+ // A directory that cannot be listed is named and skipped. Silence here
211
+ // would read as a session with no subagent activity, which is the one
212
+ // reading this audit must never produce by accident.
213
+ deps.logger(`${TAG} op=session-unreadable session=${session}` +
214
+ ` reason=${err instanceof Error ? err.message : String(err)}`);
215
+ continue;
216
+ }
217
+ for (const t of transcripts) {
218
+ live.add(t.jsonl);
219
+ const prior = seen.get(t.jsonl);
220
+ if (prior && prior.mtimeMs === t.mtimeMs)
221
+ continue;
222
+ const agentType = parseAgentType(deps.readMeta(t.meta));
223
+ let audit;
224
+ try {
225
+ audit = auditTranscript({ session, agentType, lines: deps.readLines(t.jsonl) });
226
+ }
227
+ catch (err) {
228
+ deps.logger(`${TAG} op=transcript-unreadable path=${t.jsonl}` +
229
+ ` reason=${err instanceof Error ? err.message : String(err)}`);
230
+ continue;
231
+ }
232
+ seen.set(t.jsonl, { mtimeMs: t.mtimeMs, audit });
233
+ for (const m of audit.misroutes) {
234
+ const key = `${m.session}|${m.agent}|${m.path}`;
235
+ if (logged.has(key))
236
+ continue;
237
+ deps.logger(formatMisroute(m));
238
+ logged.add(key);
239
+ }
240
+ }
241
+ }
242
+ // Drop transcripts that have gone, so the totals describe what is on disk
243
+ // now rather than accumulating across the process's whole lifetime. The
244
+ // logged-misroute keys go with them: this process runs for weeks, and a set
245
+ // that only ever grew would hold every path of every session the install
246
+ // has ever archived.
247
+ for (const path of Array.from(seen.keys()))
248
+ if (!live.has(path))
249
+ seen.delete(path);
250
+ const liveKeys = new Set();
251
+ for (const { audit } of seen.values()) {
252
+ for (const m of audit.misroutes)
253
+ liveKeys.add(`${m.session}|${m.agent}|${m.path}`);
254
+ }
255
+ for (const key of Array.from(logged))
256
+ if (!liveKeys.has(key))
257
+ logged.delete(key);
258
+ let ownedPathWrites = 0;
259
+ let misrouted = 0;
260
+ let unattributed = 0;
261
+ for (const { audit } of seen.values()) {
262
+ ownedPathWrites += audit.ownedPathWrites;
263
+ misrouted += audit.misroutes.length;
264
+ unattributed += audit.unattributed;
265
+ }
266
+ deps.logger(formatPreferenceSummary({
267
+ sessionsScanned,
268
+ transcriptsScanned: seen.size,
269
+ ownedPathWrites,
270
+ misrouted,
271
+ unattributed,
272
+ }));
273
+ }
274
+ return {
275
+ tickOnce,
276
+ start() {
277
+ if (timer)
278
+ return;
279
+ timer = setInterval(() => {
280
+ try {
281
+ tickOnce();
282
+ }
283
+ catch (err) {
284
+ deps.logger(`${TAG} op=audit-tick-failed message=${err instanceof Error ? err.message : String(err)}`);
285
+ }
286
+ }, deps.intervalMs);
287
+ if (typeof timer.unref === 'function')
288
+ timer.unref();
289
+ deps.logger(`${TAG} op=audit-started intervalMs=${deps.intervalMs}`);
290
+ },
291
+ stop() {
292
+ if (timer) {
293
+ clearInterval(timer);
294
+ timer = null;
295
+ }
296
+ },
297
+ };
298
+ }
299
+ //# sourceMappingURL=specialist-preference-audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specialist-preference-audit.js","sourceRoot":"","sources":["../src/specialist-preference-audit.ts"],"names":[],"mappings":"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"}
@@ -28,11 +28,15 @@ export interface SystemctlResult {
28
28
  * Tests inject a recording stub. */
29
29
  export type SystemctlRunner = (args: readonly string[]) => SystemctlResult;
30
30
  export declare function defaultSystemctlRunner(args: readonly string[]): SystemctlResult;
31
- export declare function scopeUnitName(unitToken: string): string;
32
- /** Inverse of `scopeUnitName` extracts the unitToken from a unit name
33
- * emitted by `systemctl --user list-units --type=scope 'claude-session-*.scope'`.
34
- * Returns null when the name doesn't match the prefix/suffix shape. */
35
- export declare function unitTokenFromScopeUnit(unit: string): string | null;
31
+ export declare function scopeUnitName(brandSlug: string, unitToken: string): string;
32
+ /** Inverse of `scopeUnitName`, for THIS brand only. Returns null for a peer
33
+ * brand's unit and for the pre-2457 unbranded shape — both are units this
34
+ * manager must never claim, reattach, or stop.
35
+ *
36
+ * A literal prefix test, not a regex: `^claude-session-(.+)\.scope$` against a
37
+ * branded name hands back `<brand>-claude-session-<token>` as the token, and a
38
+ * `maxy` slug would prefix-match a `maxy-code` unit. */
39
+ export declare function unitTokenFromScopeUnit(brandSlug: string, unit: string): string | null;
36
40
  /** One row from `systemctl --user list-units --type=scope --no-legend
37
41
  * 'claude-session-*.scope'`. The `loaded`/`active`/`sub` columns let the
38
42
  * boot-seed reconciler ignore units that are draining (`sub=stop-sigterm`)
@@ -48,21 +52,24 @@ export interface SurvivingScope {
48
52
  * Tolerant of leading column whitespace and trailing description column.
49
53
  * Rows whose unit name doesn't extract a unitToken are skipped (defensive
50
54
  * against future systemctl-output drift). */
51
- export declare function parseSurvivingScopes(stdout: string): SurvivingScope[];
55
+ export declare function parseSurvivingScopes(brandSlug: string, stdout: string): SurvivingScope[];
52
56
  /** Issue `systemctl --user list-units --type=scope --no-legend
53
57
  * 'claude-session-*.scope'` and return the parsed rows. The boot-seed
54
58
  * uses this to discover surviving PTYs across a manager restart. */
55
- export declare function listSurvivingScopes(run: SystemctlRunner): {
59
+ export declare function listSurvivingScopes(run: SystemctlRunner, brandSlug: string): {
56
60
  scopes: SurvivingScope[];
57
61
  result: SystemctlResult;
58
62
  };
59
63
  /** Stop any orphan rc scope units left over from prior session-manager
60
- * boots. Called from the session-manager startup BEFORE the rc-daemon
61
- * supervisor's first spawn and BEFORE the HTTP server begins accepting
62
- * /rc-spawn requests, so no in-flight scope can be misidentified as an
63
- * orphan. Idempotent and safe to call on every boot — if nothing
64
- * matches, no stop is issued. */
65
- export declare function reapOrphanNonPtyScopes(run: SystemctlRunner, logger: (line: string) => void): Promise<void>;
64
+ * boots OF THIS BRAND. Called from the session-manager startup BEFORE the
65
+ * rc-daemon supervisor's first spawn and BEFORE the HTTP server begins
66
+ * accepting /rc-spawn requests, so no in-flight scope can be misidentified
67
+ * as an orphan. Idempotent and safe to call on every boot — if nothing
68
+ * matches, no stop is issued.
69
+ *
70
+ * Task 2457 — `listSurvivingScopes` globs this brand only, so a peer brand's
71
+ * live session can no longer be classified as an orphan here. */
72
+ export declare function reapOrphanNonPtyScopes(run: SystemctlRunner, logger: (line: string) => void, brandSlug: string): Promise<void>;
66
73
  /** Outcome surface for a scope-stop attempt.
67
74
  *
68
75
  * Task 451 — `outcome=ok` means **unit stopped AND pid is gone**. Pre-451,
@@ -115,7 +122,7 @@ export declare const defaultProcessSignals: ProcessSignals;
115
122
  * scope's TimeoutStopSec=5 handles in-cgroup SIGTERM→SIGKILL escalation.
116
123
  * Exit code 5 ("not loaded") still means the scope retired naturally —
117
124
  * treated as `already-gone`. */
118
- export declare function stopScopeUnit(run: SystemctlRunner, unitToken: string, opts?: {
125
+ export declare function stopScopeUnit(run: SystemctlRunner, brandSlug: string, unitToken: string, opts?: {
119
126
  pid?: number | null;
120
127
  killGraceMs?: number;
121
128
  signals?: ProcessSignals;
@@ -128,6 +135,9 @@ export interface SystemdRunArgv {
128
135
  }
129
136
  export interface BuildSystemdRunArgvParams {
130
137
  unitToken: string;
138
+ /** Task 2457 — the brand that owns this scope. Lands first in the unit name
139
+ * so no peer brand's enumeration can see it. */
140
+ brandSlug: string;
131
141
  claudeBin: string;
132
142
  claudeArgs: readonly string[];
133
143
  /** Full env map to thread into the scope via `--setenv=KEY=VAL`. Every
@@ -168,7 +178,7 @@ export declare function buildSystemdRunArgv(p: BuildSystemdRunArgvParams): Syste
168
178
  * `$CLAUDE_CONFIG_DIR/sessions/<pid>.json`, which in turn carries claude's
169
179
  * intrinsic sessionId. Returns null when the unit has no main pid (the
170
180
  * scope is draining) or when the systemctl call fails. */
171
- export declare function getScopeMainPid(run: SystemctlRunner, unitToken: string): number | null;
181
+ export declare function getScopeMainPid(run: SystemctlRunner, brandSlug: string, unitToken: string): number | null;
172
182
  /** Task 1398 gap 2 — platform-branching spawn plan. macOS has no `systemd-run`;
173
183
  * wrapping a spawn in it makes node-pty exec a missing binary, which the child
174
184
  * fails as `exitCode=1 ranMs=5 bytes=0`. On darwin, spawn claude directly and
@@ -203,7 +213,7 @@ export declare function stopPidDirect(pid: number | null, opts?: {
203
213
  * the evicted pty would linger until the manager exits. This helper branches
204
214
  * on platform: darwin kills `victim.pid` directly; every other platform issues
205
215
  * exactly today's pid-omitted scope stop, byte-identical. */
206
- export declare function evictSessionProcess(platform: NodeJS.Platform, run: SystemctlRunner, victim: {
216
+ export declare function evictSessionProcess(platform: NodeJS.Platform, run: SystemctlRunner, brandSlug: string, victim: {
207
217
  scopeUnitToken: string;
208
218
  pid: number;
209
219
  }, opts?: {
@@ -1 +1 @@
1
- {"version":3,"file":"systemd-scope.d.ts","sourceRoot":"","sources":["../src/systemd-scope.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"systemd-scope.d.ts","sourceRoot":"","sources":["../src/systemd-scope.ts"],"names":[],"mappings":"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"}
@@ -59,24 +59,41 @@ export function defaultSystemctlRunner(args) {
59
59
  // token for the unit name; boot-seed reconciles unit → claude pid →
60
60
  // PID-file → claude sessionId after a restart.
61
61
  const SCOPE_NAME_VALID = /^[a-zA-Z0-9:_.\-]+$/;
62
- export function scopeUnitName(unitToken) {
62
+ // Task 2457 — the brand goes BEFORE `claude-session-`, not inside it. Every
63
+ // co-resident brand shares one `user@1000.service`, so an unbranded name let any
64
+ // brand's boot enumerate — and stop — every other brand's live scopes. The
65
+ // brand-first shape also means an un-upgraded peer, which globs
66
+ // `claude-session-*.scope`, matches none of these: a brand is protected the
67
+ // moment it upgrades, whatever state its neighbours are in.
68
+ export function scopeUnitName(brandSlug, unitToken) {
69
+ if (!brandSlug || !SCOPE_NAME_VALID.test(brandSlug)) {
70
+ throw new Error(`[systemd-scope] invalid brandSlug for unit name: ${JSON.stringify(brandSlug)}`);
71
+ }
63
72
  if (!unitToken || !SCOPE_NAME_VALID.test(unitToken)) {
64
73
  throw new Error(`[systemd-scope] invalid unitToken for unit name: ${JSON.stringify(unitToken)}`);
65
74
  }
66
- return `claude-session-${unitToken}.scope`;
75
+ return `${brandSlug}-claude-session-${unitToken}.scope`;
67
76
  }
68
- /** Inverse of `scopeUnitName` extracts the unitToken from a unit name
69
- * emitted by `systemctl --user list-units --type=scope 'claude-session-*.scope'`.
70
- * Returns null when the name doesn't match the prefix/suffix shape. */
71
- export function unitTokenFromScopeUnit(unit) {
72
- const m = unit.match(/^claude-session-(.+)\.scope$/);
73
- return m ? m[1] : null;
77
+ /** Inverse of `scopeUnitName`, for THIS brand only. Returns null for a peer
78
+ * brand's unit and for the pre-2457 unbranded shape — both are units this
79
+ * manager must never claim, reattach, or stop.
80
+ *
81
+ * A literal prefix test, not a regex: `^claude-session-(.+)\.scope$` against a
82
+ * branded name hands back `<brand>-claude-session-<token>` as the token, and a
83
+ * `maxy` slug would prefix-match a `maxy-code` unit. */
84
+ export function unitTokenFromScopeUnit(brandSlug, unit) {
85
+ const prefix = `${brandSlug}-claude-session-`;
86
+ const suffix = '.scope';
87
+ if (!unit.startsWith(prefix) || !unit.endsWith(suffix))
88
+ return null;
89
+ const token = unit.slice(prefix.length, unit.length - suffix.length);
90
+ return token.length > 0 ? token : null;
74
91
  }
75
92
  /** Parse the `--no-legend` output of `systemctl --user list-units`.
76
93
  * Tolerant of leading column whitespace and trailing description column.
77
94
  * Rows whose unit name doesn't extract a unitToken are skipped (defensive
78
95
  * against future systemctl-output drift). */
79
- export function parseSurvivingScopes(stdout) {
96
+ export function parseSurvivingScopes(brandSlug, stdout) {
80
97
  const out = [];
81
98
  for (const rawLine of stdout.split('\n')) {
82
99
  const line = rawLine.trim();
@@ -89,9 +106,10 @@ export function parseSurvivingScopes(stdout) {
89
106
  const loaded = cols[1];
90
107
  const active = cols[2];
91
108
  const sub = cols[3];
92
- if (!unitName.startsWith('claude-session-') || !unitName.endsWith('.scope'))
93
- continue;
94
- const unitToken = unitTokenFromScopeUnit(unitName);
109
+ // Task 2457 — the shape guard lives in `unitTokenFromScopeUnit`, which
110
+ // rejects a peer brand and the legacy unbranded name as well as a
111
+ // non-scope. A separate prefix test here could only disagree with it.
112
+ const unitToken = unitTokenFromScopeUnit(brandSlug, unitName);
95
113
  if (!unitToken)
96
114
  continue;
97
115
  out.push({ unitName, unitToken, loaded, active, sub });
@@ -101,43 +119,69 @@ export function parseSurvivingScopes(stdout) {
101
119
  /** Issue `systemctl --user list-units --type=scope --no-legend
102
120
  * 'claude-session-*.scope'` and return the parsed rows. The boot-seed
103
121
  * uses this to discover surviving PTYs across a manager restart. */
104
- export function listSurvivingScopes(run) {
122
+ export function listSurvivingScopes(run, brandSlug) {
105
123
  const result = run([
106
124
  '--user',
107
125
  'list-units',
108
126
  '--type=scope',
109
127
  '--no-legend',
110
128
  '--all',
111
- 'claude-session-*.scope',
129
+ // Task 2457 — the brand is IN the glob. Every consumer of this function
130
+ // (the reaper, the boot-seed reconciler, the liveness tick) inherits the
131
+ // ownership boundary from here rather than re-deriving it.
132
+ `${brandSlug}-claude-session-*.scope`,
112
133
  ]);
113
134
  if (result.exitCode !== 0) {
114
135
  return { scopes: [], result };
115
136
  }
116
- return { scopes: parseSurvivingScopes(result.stdout), result };
137
+ return { scopes: parseSurvivingScopes(brandSlug, result.stdout), result };
117
138
  }
118
139
  /** Task 554 — rc-spawn / rc-daemon scope unit-token prefixes. PTY operator
119
- * scopes use the bare `claude-session-<uuid>.scope` shape and are
140
+ * scopes use the bare `<brand>-claude-session-<uuid>.scope` shape and are
120
141
  * explicitly excluded: the boot-seed reconciler claims them as live
121
142
  * operator sessions. The reaper only sweeps tokens emitted by the
122
143
  * rc-daemon supervisor and the `/rc-spawn` HTTP route. */
123
144
  const NON_PTY_TOKEN_PREFIXES = ['rc-daemon-', 'rc-resume-'];
124
145
  /** Stop any orphan rc scope units left over from prior session-manager
125
- * boots. Called from the session-manager startup BEFORE the rc-daemon
126
- * supervisor's first spawn and BEFORE the HTTP server begins accepting
127
- * /rc-spawn requests, so no in-flight scope can be misidentified as an
128
- * orphan. Idempotent and safe to call on every boot — if nothing
129
- * matches, no stop is issued. */
130
- export async function reapOrphanNonPtyScopes(run, logger) {
131
- const { scopes, result } = listSurvivingScopes(run);
146
+ * boots OF THIS BRAND. Called from the session-manager startup BEFORE the
147
+ * rc-daemon supervisor's first spawn and BEFORE the HTTP server begins
148
+ * accepting /rc-spawn requests, so no in-flight scope can be misidentified
149
+ * as an orphan. Idempotent and safe to call on every boot — if nothing
150
+ * matches, no stop is issued.
151
+ *
152
+ * Task 2457 — `listSurvivingScopes` globs this brand only, so a peer brand's
153
+ * live session can no longer be classified as an orphan here. */
154
+ export async function reapOrphanNonPtyScopes(run, logger, brandSlug) {
155
+ const { scopes, result } = listSurvivingScopes(run, brandSlug);
132
156
  if (result.exitCode !== 0) {
133
157
  logger(`[systemd-scope] reaper list-units-failed exitCode=${result.exitCode} stderr=${result.stderr.trim()}`);
134
158
  return;
135
159
  }
160
+ // Task 2457 — count what belongs to other brands, unconditionally. Without
161
+ // this line the boundary holds silently and a regression is indistinguishable
162
+ // from a box with no other brands on it. Counted directly off the unit names,
163
+ // never by subtracting two list sizes, so a systemctl output change cannot
164
+ // turn a miscount into a plausible number.
165
+ const own = `${brandSlug}-claude-session-`;
166
+ // Two explicit patterns rather than one leading-`*` glob: `*-claude-session-*`
167
+ // covers every brand and `claude-session-*` covers the pre-2457 shape, so the
168
+ // count does not rest on how systemd's matcher treats an empty `*`.
169
+ const all = run([
170
+ '--user', 'list-units', '--type=scope', '--no-legend', '--all',
171
+ '*-claude-session-*.scope', 'claude-session-*.scope',
172
+ ]);
173
+ const foreign = all.exitCode === 0
174
+ ? all.stdout.split('\n')
175
+ .map((l) => l.trim().split(/\s+/)[0] ?? '')
176
+ .filter((u) => u.endsWith('.scope') && u.includes('claude-session-') && !u.startsWith(own))
177
+ .length
178
+ : 0;
179
+ logger(`[systemd-scope] op=foreign-scope-skipped brand=${brandSlug} skipped=${foreign}`);
136
180
  const orphans = scopes.filter((s) => NON_PTY_TOKEN_PREFIXES.some((p) => s.unitToken.startsWith(p)));
137
181
  if (orphans.length === 0)
138
182
  return;
139
183
  for (const orphan of orphans) {
140
- const stop = await stopScopeUnit(run, orphan.unitToken, {});
184
+ const stop = await stopScopeUnit(run, brandSlug, orphan.unitToken, {});
141
185
  logger(`[systemd-scope] reaped-orphan unitToken=${orphan.unitToken} active=${orphan.active} sub=${orphan.sub} outcome=${stop.outcome} exitCode=${stop.exitCode}`);
142
186
  }
143
187
  }
@@ -180,8 +224,8 @@ function sleep(ms) {
180
224
  * scope's TimeoutStopSec=5 handles in-cgroup SIGTERM→SIGKILL escalation.
181
225
  * Exit code 5 ("not loaded") still means the scope retired naturally —
182
226
  * treated as `already-gone`. */
183
- export async function stopScopeUnit(run, unitToken, opts = {}) {
184
- const unit = scopeUnitName(unitToken);
227
+ export async function stopScopeUnit(run, brandSlug, unitToken, opts = {}) {
228
+ const unit = scopeUnitName(brandSlug, unitToken);
185
229
  const result = run(['--user', 'stop', '--no-block', unit]);
186
230
  // Task 1398 gap 2 — no systemctl on this host (macOS): there is no scope to
187
231
  // stop, so terminate the pid directly. On linux the injected/default runner
@@ -190,6 +234,15 @@ export async function stopScopeUnit(run, unitToken, opts = {}) {
190
234
  return stopPidDirect(opts.pid ?? null, { killGraceMs: opts.killGraceMs, signals: opts.signals });
191
235
  }
192
236
  if (result.exitCode === 5) {
237
+ // Task 2457 — "not loaded" is only `already-gone` if the process really is
238
+ // gone. Task 451 made that the contract for `ok`; it has to hold here too.
239
+ // The rename makes this reachable: a session alive when its brand upgrades
240
+ // has an UNBRANDED token in its on-disk scope record, which `http-server`
241
+ // falls back to on End, so the branded name resolves to no unit and exit 5
242
+ // used to report success while the PTY kept running.
243
+ if (opts.pid && opts.signals?.isAlive(opts.pid) !== false) {
244
+ return stopPidDirect(opts.pid, { killGraceMs: opts.killGraceMs, signals: opts.signals });
245
+ }
193
246
  return { outcome: 'already-gone', exitCode: 5, stderr: result.stderr };
194
247
  }
195
248
  if (result.exitCode !== 0) {
@@ -242,7 +295,7 @@ export async function stopScopeUnit(run, unitToken, opts = {}) {
242
295
  * script(1) wrap (Task 556) plus the non-PTY scope primitive it served
243
296
  * are gone. */
244
297
  export function buildSystemdRunArgv(p) {
245
- const unit = scopeUnitName(p.unitToken);
298
+ const unit = scopeUnitName(p.brandSlug, p.unitToken);
246
299
  const args = [
247
300
  '--user',
248
301
  '--scope',
@@ -282,8 +335,8 @@ export function buildSystemdRunArgv(p) {
282
335
  * `$CLAUDE_CONFIG_DIR/sessions/<pid>.json`, which in turn carries claude's
283
336
  * intrinsic sessionId. Returns null when the unit has no main pid (the
284
337
  * scope is draining) or when the systemctl call fails. */
285
- export function getScopeMainPid(run, unitToken) {
286
- const unit = scopeUnitName(unitToken);
338
+ export function getScopeMainPid(run, brandSlug, unitToken) {
339
+ const unit = scopeUnitName(brandSlug, unitToken);
287
340
  const result = run(['--user', 'show', '-p', 'MainPID', '--value', unit]);
288
341
  if (result.exitCode !== 0)
289
342
  return null;
@@ -347,10 +400,10 @@ export async function stopPidDirect(pid, opts = {}) {
347
400
  * the evicted pty would linger until the manager exits. This helper branches
348
401
  * on platform: darwin kills `victim.pid` directly; every other platform issues
349
402
  * exactly today's pid-omitted scope stop, byte-identical. */
350
- export async function evictSessionProcess(platform, run, victim, opts = {}) {
403
+ export async function evictSessionProcess(platform, run, brandSlug, victim, opts = {}) {
351
404
  if (platform === 'darwin') {
352
405
  return stopPidDirect(victim.pid, opts);
353
406
  }
354
- return stopScopeUnit(run, victim.scopeUnitToken);
407
+ return stopScopeUnit(run, brandSlug, victim.scopeUnitToken);
355
408
  }
356
409
  //# sourceMappingURL=systemd-scope.js.map