@rubytech/create-maxy-code 0.1.91 → 0.1.93
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.
- package/dist/__tests__/claude-ptys-slice.test.js +25 -0
- package/dist/index.js +10 -1
- package/dist/port-resolution.js +29 -0
- package/package.json +1 -1
- package/payload/platform/package-lock.json +18 -4
- package/payload/platform/plugins/docs/references/platform.md +4 -2
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +65 -5
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +468 -223
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +10 -3
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +486 -193
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +72 -13
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +10 -4
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +27 -8
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts +17 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js +25 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +165 -33
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +290 -91
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts +6 -2
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.js +29 -25
- package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +55 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +170 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +6 -4
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.js +8 -10
- package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +114 -0
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +209 -0
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/types.d.ts +0 -4
- package/payload/platform/services/claude-session-manager/dist/types.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/user-title-store.d.ts +39 -0
- package/payload/platform/services/claude-session-manager/dist/user-title-store.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/user-title-store.js +138 -0
- package/payload/platform/services/claude-session-manager/dist/user-title-store.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +3 -0
- package/payload/server/public/assets/{ChatInput-dLWJ0Q8I.css → ChatInput-CzQhPt90.css} +1 -1
- package/payload/server/public/assets/{Checkbox-Bx8mRHQ3.js → Checkbox-ynZtDWi3.js} +1 -1
- package/payload/server/public/assets/admin-ux6TKHS1.js +217 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BEW_bD50.js → architectureDiagram-Q4EWVU46-Ko_izKmY.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DFLT98C3.js → blockDiagram-DXYQGD6D-D58VDC0w.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-BemKtROA.js → c4Diagram-AHTNJAMY-C-eRXyKi.js} +1 -1
- package/payload/server/public/assets/channel-CejaITMF.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-BL8fGJ7_.js → chunk-336JU56O-DZLEfLAb.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-BD6by0MP.js → chunk-426QAEUC-BriTU3L8.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-DX-cb6Nd.js → chunk-4TB4RGXK-BdxRL-AY.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-B2QqtIZS.js → chunk-5FUZZQ4R-BgEmy7Y3.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-CzJWmsmd.js → chunk-5PVQY5BW-emnbDRLA.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-DP9vcvEU.js → chunk-EDXVE4YY-BvCf2Xf2.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-D5xWEjF0.js → chunk-ENJZ2VHE-DTpEYJRb.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-AKduf9o4.js → chunk-ICPOFSXX-Ced2j48Y.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-D6jsVNyQ.js → chunk-OYMX7WX6-B21_a3Dv.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-BNAnTYh8.js → chunk-U2HBQHQK-CPS3yyXK.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-UdQhYDnh.js → chunk-X2U36JSP-BJ_B5NW7.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-DSSmSGmS.js → chunk-YZCP3GAM-CMLy-WXm.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-Czm7IDMC.js → chunk-ZZ45TVLE-BkLnzCHe.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BmoqALf4.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-vMajwCNi.js +1 -0
- package/payload/server/public/assets/clone-mvVfHkyg.js +1 -0
- package/payload/server/public/assets/{dagre-hSIMHJcO.js → dagre-Dd_gq4yj.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-CBuRCJE7.js → dagre-KV5264BT-RY0E5UAn.js} +1 -1
- package/payload/server/public/assets/data-BK4r8Dqy.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-dbMrH0le.js → diagram-5BDNPKRD-DR7XiPLh.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-DBc6hs9a.js → diagram-G4DWMVQ6-2EbOfKFs.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-ubSGybUk.js → diagram-MMDJMWI5-CGcY0Ze7.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-WxL3cv-9.js → diagram-TYMM5635-Dk6MmAso.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-CrBBOAN8.js → erDiagram-SMLLAGMA-CKvdlUqk.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CXUq5Hzt.js → flowDiagram-DWJPFMVM-BqKhvCDz.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-vTCBnYVS.js → ganttDiagram-T4ZO3ILL-CeiE-IRb.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BHrj_bRa.js → gitGraphDiagram-UUTBAWPF-BbPKn7v2.js} +1 -1
- package/payload/server/public/assets/graph-Cb6A8Hhs.js +1 -0
- package/payload/server/public/assets/graph-labels-DCsUNtnE.js +1 -0
- package/payload/server/public/assets/{graphlib-CDO75l2X.js → graphlib-tG6kR59B.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CcQdaOhi.js → infoDiagram-42DDH7IO-6MiRGCI1.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-3KvltFgV.js → ishikawaDiagram-UXIWVN3A-BwY4p5mg.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DZCXaGFE.js → journeyDiagram-VCZTEJTY-BXAQ_fUi.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DAIAKy1-.js → kanban-definition-6JOO6SKY-ChiiO7Yt.js} +1 -1
- package/payload/server/public/assets/{lib-CCLURO3E.js → lib-DbPz9cfu.js} +1 -1
- package/payload/server/public/assets/{line-C8iCE2Wx.js → line-B5g5OrOD.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-ElvUhnV9.js → mermaid-parser.core-BMGzMKJx.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-7dSH31Ao.js → mermaid.core-DLMgkjXn.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-FZ0rcfac.js → mindmap-definition-QFDTVHPH-BvyzRLAi.js} +1 -1
- package/payload/server/public/assets/{page-D3bFmqF4.js → page-BeYR_GI2.js} +1 -1
- package/payload/server/public/assets/{page-DNjUtT5q.js → page-DVNp-Dfy.js} +2 -2
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CDuvMZbO.js → pieDiagram-DEJITSTG--EV9pVfs.js} +1 -1
- package/payload/server/public/assets/{public-g_2skbnY.js → public-sYW0Dwku.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-xDjhkqNc.js → quadrantDiagram-34T5L4WZ-DJcL08ft.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-BPKpyT0n.js → requirementDiagram-MS252O5E-D4VdmmSu.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BvFIrihC.js → sankeyDiagram-XADWPNL6-Dsw3nHRS.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-C6G1oAE0.js → sequenceDiagram-FGHM5R23-CAgemUx5.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DbOJT_Xh.js → stateDiagram-FHFEXIEX-B7dyL9_X.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CImN6Wk6.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D1DSxHh6.js → timeline-definition-GMOUNBTQ-B7iKswhp.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BYoSeMdo.js → vennDiagram-DHZGUBPP-uFS0i1qX.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-DH3kZdea.js → wardleyDiagram-NUSXRM2D-lDUh4LAD.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-C6MROMLG.js → xychartDiagram-5P7HB3ND-0v7MEVwW.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +17 -0
- package/payload/server/public/assets/admin-C57bDUgs.js +0 -217
- package/payload/server/public/assets/channel-CfpByn-m.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DNc9R1IX.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DM1DQh8N.js +0 -1
- package/payload/server/public/assets/clone-BSrNdvLW.js +0 -1
- package/payload/server/public/assets/data-3n20lir5.js +0 -1
- package/payload/server/public/assets/graph-ChwMpxn_.js +0 -1
- package/payload/server/public/assets/graph-labels-CUgJgIwl.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ce4GFk-H.js +0 -1
- /package/payload/server/public/assets/{ChatInput-DoS80kaC.js → ChatInput-Cj9JQtYt.js} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,wEAAwE;AACxE,sEAAsE;AACtE,yEAAyE;AACzE,uEAAuE;AACvE,oEAAoE;AACpE,EAAE;AACF,aAAa;AACb,oEAAoE;AACpE,yEAAyE;AACzE,uEAAuE;AACvE,oEAAoE;AACpE,kEAAkE;AAClE,uEAAuE;AACvE,0DAA0D;AAC1D,wEAAwE;AACxE,8DAA8D;AAyC9D,MAAM,OAAO,YAAY;IACN,WAAW,GAAG,IAAI,GAAG,EAAyB,CAAA;IAE/D,GAAG,CAAC,CAAgB;QAClB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IACtC,CAAC;IAED,GAAG,CAAC,SAAiB;QACnB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACxC,CAAC;IAED,MAAM,CAAC,SAAiB;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACzC,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAA;QACxB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QAClC,OAAO,CAAC,CAAA;IACV,CAAC;IAED,YAAY,CAAC,QAAgB,EAAE,OAAoC,EAAE;QACnE,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,KAAK,IAAI,CAAA;QACjD,MAAM,GAAG,GAAoB,EAAE,CAAA;QAC/B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ;gBAAE,SAAQ;YACrC,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,MAAM;gBAAE,SAAQ;YACxC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACb,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,GAAG;QACD,OAAO,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAA;IACvC,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,CAAA;IAC9B,CAAC;CACF"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Single source of truth for the slice unit name. Used by the spawn argv,
|
|
2
|
+
* the boot-seed list-units filter, and the installer's slice unit
|
|
3
|
+
* emission. */
|
|
4
|
+
export declare const CLAUDE_PTYS_SLICE = "claude-ptys.slice";
|
|
5
|
+
/** Hard cap on SIGTERM→SIGKILL escalation inside the scope. systemd
|
|
6
|
+
* delivers SIGTERM on stop, waits TimeoutStopSec, then SIGKILL. 5 s
|
|
7
|
+
* matches the manager's prior killGraceMs (`config.killGraceMs`); the
|
|
8
|
+
* manager no longer manages the escalation itself. */
|
|
9
|
+
export declare const SCOPE_TIMEOUT_STOP_SEC = 5;
|
|
10
|
+
/** Result of one systemctl invocation. Mirrors the shape of
|
|
11
|
+
* spawnSync('utf8') return — exit code plus captured streams. The
|
|
12
|
+
* scope-stop and list-units paths both go through this shape so tests
|
|
13
|
+
* can inject a single stub. */
|
|
14
|
+
export interface SystemctlResult {
|
|
15
|
+
exitCode: number;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
}
|
|
19
|
+
/** Synchronous runner for `systemctl <args...>`. Default impl uses
|
|
20
|
+
* spawnSync (matches uninstall.ts's existing idiom and keeps the call
|
|
21
|
+
* site free of async glue for what is in practice a sub-50ms call).
|
|
22
|
+
* Tests inject a recording stub. */
|
|
23
|
+
export type SystemctlRunner = (args: readonly string[]) => SystemctlResult;
|
|
24
|
+
export declare function defaultSystemctlRunner(args: readonly string[]): SystemctlResult;
|
|
25
|
+
export declare function scopeUnitName(unitToken: string): string;
|
|
26
|
+
/** Inverse of `scopeUnitName` — extracts the unitToken from a unit name
|
|
27
|
+
* emitted by `systemctl --user list-units --type=scope 'claude-session-*.scope'`.
|
|
28
|
+
* Returns null when the name doesn't match the prefix/suffix shape. */
|
|
29
|
+
export declare function unitTokenFromScopeUnit(unit: string): string | null;
|
|
30
|
+
/** One row from `systemctl --user list-units --type=scope --no-legend
|
|
31
|
+
* 'claude-session-*.scope'`. The `loaded`/`active`/`sub` columns let the
|
|
32
|
+
* boot-seed reconciler ignore units that are draining (`sub=stop-sigterm`)
|
|
33
|
+
* vs running (`sub=running`). */
|
|
34
|
+
export interface SurvivingScope {
|
|
35
|
+
unitName: string;
|
|
36
|
+
unitToken: string;
|
|
37
|
+
loaded: string;
|
|
38
|
+
active: string;
|
|
39
|
+
sub: string;
|
|
40
|
+
}
|
|
41
|
+
/** Parse the `--no-legend` output of `systemctl --user list-units`.
|
|
42
|
+
* Tolerant of leading column whitespace and trailing description column.
|
|
43
|
+
* Rows whose unit name doesn't extract a unitToken are skipped (defensive
|
|
44
|
+
* against future systemctl-output drift). */
|
|
45
|
+
export declare function parseSurvivingScopes(stdout: string): SurvivingScope[];
|
|
46
|
+
/** Issue `systemctl --user list-units --type=scope --no-legend
|
|
47
|
+
* 'claude-session-*.scope'` and return the parsed rows. The boot-seed
|
|
48
|
+
* uses this to discover surviving PTYs across a manager restart. */
|
|
49
|
+
export declare function listSurvivingScopes(run: SystemctlRunner): {
|
|
50
|
+
scopes: SurvivingScope[];
|
|
51
|
+
result: SystemctlResult;
|
|
52
|
+
};
|
|
53
|
+
/** Outcome surface for a scope-stop attempt. `already-gone` is the
|
|
54
|
+
* idempotent success case: systemd reports the unit isn't loaded
|
|
55
|
+
* (exit code 5) because claude exited naturally and systemd retired the
|
|
56
|
+
* scope. */
|
|
57
|
+
export type StopScopeOutcome = 'ok' | 'already-gone' | 'error';
|
|
58
|
+
export interface StopScopeResult {
|
|
59
|
+
outcome: StopScopeOutcome;
|
|
60
|
+
exitCode: number;
|
|
61
|
+
stderr: string;
|
|
62
|
+
}
|
|
63
|
+
/** Issue `systemctl --user stop --no-block <unit>`. The --no-block flag
|
|
64
|
+
* returns once the stop request is queued; the scope's TimeoutStopSec=5
|
|
65
|
+
* handles SIGTERM→SIGKILL escalation. Exit code 5 ("not loaded") means
|
|
66
|
+
* the scope retired naturally — treated as success. */
|
|
67
|
+
export declare function stopScopeUnit(run: SystemctlRunner, unitToken: string): StopScopeResult;
|
|
68
|
+
/** Argv shape produced by `buildSystemdRunArgv`. The cmd is always
|
|
69
|
+
* `systemd-run`; the manager passes this to node-pty. */
|
|
70
|
+
export interface SystemdRunArgv {
|
|
71
|
+
cmd: 'systemd-run';
|
|
72
|
+
args: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface BuildSystemdRunArgvParams {
|
|
75
|
+
unitToken: string;
|
|
76
|
+
claudeBin: string;
|
|
77
|
+
claudeArgs: readonly string[];
|
|
78
|
+
/** Full env map to thread into the scope via `--setenv=KEY=VAL`. Every
|
|
79
|
+
* pair lands as one flag. The default systemd-run inherit-from-caller
|
|
80
|
+
* is NOT relied on; spawnPtyAdapter composes the explicit env block
|
|
81
|
+
* (per-spawn overrides + a copy of process.env it wants in the scope)
|
|
82
|
+
* and the helper threads exactly that shape. */
|
|
83
|
+
env: Readonly<Record<string, string>>;
|
|
84
|
+
/** Working directory passed via `--working-directory=`. The manager's
|
|
85
|
+
* prior node-pty spawn passed `cwd`; the scope receives it the same way. */
|
|
86
|
+
cwd: string;
|
|
87
|
+
}
|
|
88
|
+
/** Build the systemd-run argv that node-pty wraps for one claude spawn.
|
|
89
|
+
* Inside the scope, `/bin/sh -c 'trap "" HUP; exec "$@"'` wraps the
|
|
90
|
+
* launch so claude ignores SIGHUP when the manager closes the PTY master
|
|
91
|
+
* fd on restart. Without the wrap, master-close → kernel SIGHUP → claude
|
|
92
|
+
* exits, and the brief's "PTY pid survives manager restart" verify-gate
|
|
93
|
+
* fails.
|
|
94
|
+
*
|
|
95
|
+
* The shell `exec` replaces the sh process image with claude in-place
|
|
96
|
+
* rather than forking, so pty.pid == claude.pid (the pid is preserved
|
|
97
|
+
* across execve). This matters because the manager's
|
|
98
|
+
* `watcher.waitForPid(pty.pid, …)` resolves when Claude Code writes its
|
|
99
|
+
* own PID file at `$CLAUDE_CONFIG_DIR/sessions/<claude-pid>.json`; with
|
|
100
|
+
* a non-exec sh wrap, pty.pid would be sh's pid (≠ claude's pid) and
|
|
101
|
+
* waitForPid would never resolve. */
|
|
102
|
+
export declare function buildSystemdRunArgv(p: BuildSystemdRunArgvParams): SystemdRunArgv;
|
|
103
|
+
/** Query `systemctl --user show -p MainPID claude-session-<token>.scope`
|
|
104
|
+
* and return the main pid (the claude process). Boot-seed uses this to
|
|
105
|
+
* bridge from the surviving scope unit to the PID file claude wrote at
|
|
106
|
+
* `$CLAUDE_CONFIG_DIR/sessions/<pid>.json`, which in turn carries claude's
|
|
107
|
+
* intrinsic sessionId. Returns null when the unit has no main pid (the
|
|
108
|
+
* scope is draining) or when the systemctl call fails. */
|
|
109
|
+
export declare function getScopeMainPid(run: SystemctlRunner, unitToken: string): number | null;
|
|
110
|
+
/** Slice unit declared at install time so every claude scope shares one
|
|
111
|
+
* cohort cgroup. `systemctl --user status claude-ptys.slice` gives the
|
|
112
|
+
* operator a one-glance view of total live-session memory + CPU. */
|
|
113
|
+
export declare function buildClaudePtysSliceUnitFile(): string;
|
|
114
|
+
//# sourceMappingURL=systemd-scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemd-scope.d.ts","sourceRoot":"","sources":["../src/systemd-scope.ts"],"names":[],"mappings":"AAuBA;;gBAEgB;AAChB,eAAO,MAAM,iBAAiB,sBAAsB,CAAA;AAEpD;;;uDAGuD;AACvD,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC;;;gCAGgC;AAChC,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;qCAGqC;AACrC,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,eAAe,CAAA;AAE1E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,CAU/E;AAiBD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOvD;AAED;;wEAEwE;AACxE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED;;;kCAGkC;AAClC,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;8CAG8C;AAC9C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAiBrE;AAED;;qEAEqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,GAAG;IACzD,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,MAAM,EAAE,eAAe,CAAA;CACxB,CAaA;AAED;;;aAGa;AACb,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,cAAc,GAAG,OAAO,CAAA;AAE9D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,gBAAgB,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;wDAGwD;AACxD,wBAAgB,aAAa,CAC3B,GAAG,EAAE,eAAe,EACpB,SAAS,EAAE,MAAM,GAChB,eAAe,CAcjB;AAED;0DAC0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,aAAa,CAAA;IAClB,IAAI,EAAE,MAAM,EAAE,CAAA;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B;;;;qDAIiD;IACjD,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACrC;iFAC6E;IAC7E,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;;;;;;;;sCAasC;AACtC,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,yBAAyB,GAAG,cAAc,CA8BhF;AAED;;;;;2DAK2D;AAC3D,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,EACpB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAQf;AAED;;qEAEqE;AACrE,wBAAgB,4BAA4B,IAAI,MAAM,CAWrD"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Task 250 — every `claude --verbose` PTY spawn lives in its own systemd
|
|
2
|
+
// transient scope unit under a shared `claude-ptys.slice`, instead of inside
|
|
3
|
+
// the session-manager unit's cgroup. The scope cgroup is owned by user
|
|
4
|
+
// systemd, not by the manager; stopping the manager unit (installer rerun,
|
|
5
|
+
// crash, watchdog, restart) no longer cascades a control-group kill to
|
|
6
|
+
// active operator PTYs.
|
|
7
|
+
//
|
|
8
|
+
// This module holds the pure argv/parser helpers that the spawn adapter,
|
|
9
|
+
// stop path, reaper, boot-seed, and the installer share. I/O happens via an
|
|
10
|
+
// injected systemctl callable so tests can stub it without invoking real
|
|
11
|
+
// systemd. node-pty owns the spawn of `systemd-run` itself — this module
|
|
12
|
+
// never touches node-pty.
|
|
13
|
+
//
|
|
14
|
+
// SIGHUP wrap. Inside the scope, the claude invocation is wrapped with
|
|
15
|
+
// `/bin/sh -c 'trap "" HUP; <invoke> "$@"' --`. The wrap exists because
|
|
16
|
+
// closing the PTY master fd (which the manager owns) makes the kernel
|
|
17
|
+
// deliver SIGHUP to the foreground process group of the slave session —
|
|
18
|
+
// and claude's default disposition is exit. With SIGHUP set to ignore,
|
|
19
|
+
// claude survives the master closing on manager restart, which is what
|
|
20
|
+
// the cgroup separation is there to enable.
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
/** Single source of truth for the slice unit name. Used by the spawn argv,
|
|
23
|
+
* the boot-seed list-units filter, and the installer's slice unit
|
|
24
|
+
* emission. */
|
|
25
|
+
export const CLAUDE_PTYS_SLICE = 'claude-ptys.slice';
|
|
26
|
+
/** Hard cap on SIGTERM→SIGKILL escalation inside the scope. systemd
|
|
27
|
+
* delivers SIGTERM on stop, waits TimeoutStopSec, then SIGKILL. 5 s
|
|
28
|
+
* matches the manager's prior killGraceMs (`config.killGraceMs`); the
|
|
29
|
+
* manager no longer manages the escalation itself. */
|
|
30
|
+
export const SCOPE_TIMEOUT_STOP_SEC = 5;
|
|
31
|
+
export function defaultSystemctlRunner(args) {
|
|
32
|
+
const r = spawnSync('systemctl', [...args], {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
timeout: 10_000,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
exitCode: typeof r.status === 'number' ? r.status : 1,
|
|
38
|
+
stdout: typeof r.stdout === 'string' ? r.stdout : '',
|
|
39
|
+
stderr: typeof r.stderr === 'string' ? r.stderr : '',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// systemd unit names accept `[a-zA-Z0-9:_.\-]`. The unit-token is a
|
|
43
|
+
// per-spawn stable id the manager passes in: for resume spawns it equals
|
|
44
|
+
// Claude Code's intrinsic sessionId (a UUID, which satisfies the char
|
|
45
|
+
// set); for fresh spawns it's a manager-minted UUID. Either way the
|
|
46
|
+
// helper refuses any token that doesn't match the systemd char set
|
|
47
|
+
// rather than silently rewriting — preserving the lookup-by-name contract
|
|
48
|
+
// the brief depends on ("no clever uniqueness suffixes").
|
|
49
|
+
//
|
|
50
|
+
// Why the token isn't always Claude Code's sessionId: at fresh-spawn
|
|
51
|
+
// time the manager has no sessionId yet (Claude Code mints it during
|
|
52
|
+
// boot and writes it to its PID file). The manager mints a separate
|
|
53
|
+
// token for the unit name; boot-seed reconciles unit → claude pid →
|
|
54
|
+
// PID-file → claude sessionId after a restart.
|
|
55
|
+
const SCOPE_NAME_VALID = /^[a-zA-Z0-9:_.\-]+$/;
|
|
56
|
+
export function scopeUnitName(unitToken) {
|
|
57
|
+
if (!unitToken || !SCOPE_NAME_VALID.test(unitToken)) {
|
|
58
|
+
throw new Error(`[systemd-scope] invalid unitToken for unit name: ${JSON.stringify(unitToken)}`);
|
|
59
|
+
}
|
|
60
|
+
return `claude-session-${unitToken}.scope`;
|
|
61
|
+
}
|
|
62
|
+
/** Inverse of `scopeUnitName` — extracts the unitToken from a unit name
|
|
63
|
+
* emitted by `systemctl --user list-units --type=scope 'claude-session-*.scope'`.
|
|
64
|
+
* Returns null when the name doesn't match the prefix/suffix shape. */
|
|
65
|
+
export function unitTokenFromScopeUnit(unit) {
|
|
66
|
+
const m = unit.match(/^claude-session-(.+)\.scope$/);
|
|
67
|
+
return m ? m[1] : null;
|
|
68
|
+
}
|
|
69
|
+
/** Parse the `--no-legend` output of `systemctl --user list-units`.
|
|
70
|
+
* Tolerant of leading column whitespace and trailing description column.
|
|
71
|
+
* Rows whose unit name doesn't extract a unitToken are skipped (defensive
|
|
72
|
+
* against future systemctl-output drift). */
|
|
73
|
+
export function parseSurvivingScopes(stdout) {
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const rawLine of stdout.split('\n')) {
|
|
76
|
+
const line = rawLine.trim();
|
|
77
|
+
if (!line)
|
|
78
|
+
continue;
|
|
79
|
+
const cols = line.split(/\s+/);
|
|
80
|
+
if (cols.length < 4)
|
|
81
|
+
continue;
|
|
82
|
+
const unitName = cols[0];
|
|
83
|
+
const loaded = cols[1];
|
|
84
|
+
const active = cols[2];
|
|
85
|
+
const sub = cols[3];
|
|
86
|
+
if (!unitName.startsWith('claude-session-') || !unitName.endsWith('.scope'))
|
|
87
|
+
continue;
|
|
88
|
+
const unitToken = unitTokenFromScopeUnit(unitName);
|
|
89
|
+
if (!unitToken)
|
|
90
|
+
continue;
|
|
91
|
+
out.push({ unitName, unitToken, loaded, active, sub });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** Issue `systemctl --user list-units --type=scope --no-legend
|
|
96
|
+
* 'claude-session-*.scope'` and return the parsed rows. The boot-seed
|
|
97
|
+
* uses this to discover surviving PTYs across a manager restart. */
|
|
98
|
+
export function listSurvivingScopes(run) {
|
|
99
|
+
const result = run([
|
|
100
|
+
'--user',
|
|
101
|
+
'list-units',
|
|
102
|
+
'--type=scope',
|
|
103
|
+
'--no-legend',
|
|
104
|
+
'--all',
|
|
105
|
+
'claude-session-*.scope',
|
|
106
|
+
]);
|
|
107
|
+
if (result.exitCode !== 0) {
|
|
108
|
+
return { scopes: [], result };
|
|
109
|
+
}
|
|
110
|
+
return { scopes: parseSurvivingScopes(result.stdout), result };
|
|
111
|
+
}
|
|
112
|
+
/** Issue `systemctl --user stop --no-block <unit>`. The --no-block flag
|
|
113
|
+
* returns once the stop request is queued; the scope's TimeoutStopSec=5
|
|
114
|
+
* handles SIGTERM→SIGKILL escalation. Exit code 5 ("not loaded") means
|
|
115
|
+
* the scope retired naturally — treated as success. */
|
|
116
|
+
export function stopScopeUnit(run, unitToken) {
|
|
117
|
+
const unit = scopeUnitName(unitToken);
|
|
118
|
+
const result = run(['--user', 'stop', '--no-block', unit]);
|
|
119
|
+
if (result.exitCode === 0) {
|
|
120
|
+
return { outcome: 'ok', exitCode: 0, stderr: result.stderr };
|
|
121
|
+
}
|
|
122
|
+
// systemctl exits 5 when the unit isn't loaded — i.e. systemd already
|
|
123
|
+
// retired the scope because claude exited on its own. Operator pressed
|
|
124
|
+
// End on a row whose claude had already quit; we treat the request as
|
|
125
|
+
// satisfied.
|
|
126
|
+
if (result.exitCode === 5) {
|
|
127
|
+
return { outcome: 'already-gone', exitCode: 5, stderr: result.stderr };
|
|
128
|
+
}
|
|
129
|
+
return { outcome: 'error', exitCode: result.exitCode, stderr: result.stderr };
|
|
130
|
+
}
|
|
131
|
+
/** Build the systemd-run argv that node-pty wraps for one claude spawn.
|
|
132
|
+
* Inside the scope, `/bin/sh -c 'trap "" HUP; exec "$@"'` wraps the
|
|
133
|
+
* launch so claude ignores SIGHUP when the manager closes the PTY master
|
|
134
|
+
* fd on restart. Without the wrap, master-close → kernel SIGHUP → claude
|
|
135
|
+
* exits, and the brief's "PTY pid survives manager restart" verify-gate
|
|
136
|
+
* fails.
|
|
137
|
+
*
|
|
138
|
+
* The shell `exec` replaces the sh process image with claude in-place
|
|
139
|
+
* rather than forking, so pty.pid == claude.pid (the pid is preserved
|
|
140
|
+
* across execve). This matters because the manager's
|
|
141
|
+
* `watcher.waitForPid(pty.pid, …)` resolves when Claude Code writes its
|
|
142
|
+
* own PID file at `$CLAUDE_CONFIG_DIR/sessions/<claude-pid>.json`; with
|
|
143
|
+
* a non-exec sh wrap, pty.pid would be sh's pid (≠ claude's pid) and
|
|
144
|
+
* waitForPid would never resolve. */
|
|
145
|
+
export function buildSystemdRunArgv(p) {
|
|
146
|
+
const unit = scopeUnitName(p.unitToken);
|
|
147
|
+
const args = [
|
|
148
|
+
'--user',
|
|
149
|
+
'--scope',
|
|
150
|
+
'--quiet',
|
|
151
|
+
`--unit=${unit}`,
|
|
152
|
+
`--slice=${CLAUDE_PTYS_SLICE}`,
|
|
153
|
+
'-p',
|
|
154
|
+
`TimeoutStopSec=${SCOPE_TIMEOUT_STOP_SEC}`,
|
|
155
|
+
`--working-directory=${p.cwd}`,
|
|
156
|
+
];
|
|
157
|
+
// One --setenv per env key. Order is sorted-by-key for deterministic
|
|
158
|
+
// log lines and test fixtures (and to make argv-diff review tractable
|
|
159
|
+
// across spawns).
|
|
160
|
+
const keys = Object.keys(p.env).sort();
|
|
161
|
+
for (const k of keys) {
|
|
162
|
+
args.push(`--setenv=${k}=${p.env[k]}`);
|
|
163
|
+
}
|
|
164
|
+
// POSIX `--` sentinel — ends systemd-run's own flag parsing so the
|
|
165
|
+
// following positional argv (the sh wrapper + claude) doesn't get
|
|
166
|
+
// swallowed by `--setenv=` etc.
|
|
167
|
+
args.push('--');
|
|
168
|
+
// SIGHUP-trap wrap. `$@` expands every argv from $0 onwards INSIDE the
|
|
169
|
+
// shell, so passing 'sh' as $0 lets `"$@"` resolve to
|
|
170
|
+
// `<claudeBin> <claudeArgs...>` after the trap. Without the explicit
|
|
171
|
+
// $0 ('sh') the first claudeArg would be consumed as $0 and lost.
|
|
172
|
+
args.push('/bin/sh', '-c', 'trap "" HUP; exec "$@"', 'sh', p.claudeBin);
|
|
173
|
+
for (const a of p.claudeArgs)
|
|
174
|
+
args.push(a);
|
|
175
|
+
return { cmd: 'systemd-run', args };
|
|
176
|
+
}
|
|
177
|
+
/** Query `systemctl --user show -p MainPID claude-session-<token>.scope`
|
|
178
|
+
* and return the main pid (the claude process). Boot-seed uses this to
|
|
179
|
+
* bridge from the surviving scope unit to the PID file claude wrote at
|
|
180
|
+
* `$CLAUDE_CONFIG_DIR/sessions/<pid>.json`, which in turn carries claude's
|
|
181
|
+
* intrinsic sessionId. Returns null when the unit has no main pid (the
|
|
182
|
+
* scope is draining) or when the systemctl call fails. */
|
|
183
|
+
export function getScopeMainPid(run, unitToken) {
|
|
184
|
+
const unit = scopeUnitName(unitToken);
|
|
185
|
+
const result = run(['--user', 'show', '-p', 'MainPID', '--value', unit]);
|
|
186
|
+
if (result.exitCode !== 0)
|
|
187
|
+
return null;
|
|
188
|
+
const raw = result.stdout.trim();
|
|
189
|
+
if (!raw || raw === '0')
|
|
190
|
+
return null;
|
|
191
|
+
const n = Number.parseInt(raw, 10);
|
|
192
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
193
|
+
}
|
|
194
|
+
/** Slice unit declared at install time so every claude scope shares one
|
|
195
|
+
* cohort cgroup. `systemctl --user status claude-ptys.slice` gives the
|
|
196
|
+
* operator a one-glance view of total live-session memory + CPU. */
|
|
197
|
+
export function buildClaudePtysSliceUnitFile() {
|
|
198
|
+
return `[Unit]
|
|
199
|
+
Description=Claude Code PTY sessions (Task 250)
|
|
200
|
+
Documentation=https://github.com/rubytech/maxy-code/blob/main/.tasks/archive/250-claude-ptys-survive-manager-restart-via-systemd-scope-per-pty.md
|
|
201
|
+
Before=shutdown.target
|
|
202
|
+
|
|
203
|
+
[Slice]
|
|
204
|
+
|
|
205
|
+
[Install]
|
|
206
|
+
WantedBy=default.target
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=systemd-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemd-scope.js","sourceRoot":"","sources":["../src/systemd-scope.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,10 +17,6 @@ export interface Session {
|
|
|
17
17
|
senderId: string;
|
|
18
18
|
role: Role;
|
|
19
19
|
channel: Channel;
|
|
20
|
-
/** Path of the JSONL Claude Code wrote, observed from disk. Non-null
|
|
21
|
-
* on every wire payload — the manager blocks the spawn response on
|
|
22
|
-
* observation, so a session never appears in /list without it. */
|
|
23
|
-
jsonlPath: string;
|
|
24
20
|
}
|
|
25
21
|
export interface PtyHandle {
|
|
26
22
|
readonly pid: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAA;AAErC,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAA;AAE/E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,OAAO;IACtB;;;2DAGuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAA;AAErC,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAA;AAE/E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,OAAO;IACtB;;;2DAGuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,CAAA;IACvD,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,CAAA;IACnF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B;;;wEAGoE;IACpE,OAAO,CAAC,IAAI,IAAI,CAAA;CACjB;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Logger } from './types.js';
|
|
2
|
+
export interface UserTitleEntry {
|
|
3
|
+
title: string;
|
|
4
|
+
renamedAt: string;
|
|
5
|
+
}
|
|
6
|
+
export type ValidateResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
title: string;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: 'empty' | 'too-long' | 'control-chars';
|
|
12
|
+
};
|
|
13
|
+
/** Single source of truth for user-title validation. Mirrored in
|
|
14
|
+
* Sidebar.tsx + SessionMetadataPane.tsx as length-only client-side checks;
|
|
15
|
+
* the control-char rejection is server-only. */
|
|
16
|
+
export declare function validateUserTitle(raw: unknown): ValidateResult;
|
|
17
|
+
export declare class UserTitleStore {
|
|
18
|
+
private readonly bySessionId;
|
|
19
|
+
private readonly filePath;
|
|
20
|
+
private readonly logger;
|
|
21
|
+
private loaded;
|
|
22
|
+
constructor(filePath: string, logger: Logger);
|
|
23
|
+
/** Read the persisted file into the in-memory map. Boot-resilient: any
|
|
24
|
+
* failure (missing file, malformed JSON, permission denied) is logged
|
|
25
|
+
* and the store starts empty. Never throws. */
|
|
26
|
+
loadFromDisk(): void;
|
|
27
|
+
get(sessionId: string): UserTitleEntry | undefined;
|
|
28
|
+
/** Persist `title` for `sessionId`. Caller is responsible for validating
|
|
29
|
+
* via `validateUserTitle` first; the store stores whatever it's given. */
|
|
30
|
+
set(sessionId: string, title: string): UserTitleEntry;
|
|
31
|
+
/** Remove the entry for `sessionId` if present. Called on session
|
|
32
|
+
* purge/delete cascade so the file doesn't accrete orphans.
|
|
33
|
+
* Returns true when an entry was removed. */
|
|
34
|
+
remove(sessionId: string): boolean;
|
|
35
|
+
/** Current in-memory snapshot — diagnostic only. */
|
|
36
|
+
size(): number;
|
|
37
|
+
private persistAtomic;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=user-title-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-title-store.d.ts","sourceRoot":"","sources":["../src/user-title-store.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,eAAe,CAAA;CAAE,CAAA;AAKjE;;iDAEiD;AACjD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAO9D;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,MAAM,CAAQ;gBAEV,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAK5C;;oDAEgD;IAChD,YAAY,IAAI,IAAI;IA0CpB,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAIlD;+EAC2E;IAC3E,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc;IAOrD;;kDAE8C;IAC9C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAMlC,oDAAoD;IACpD,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,aAAa;CActB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Persistent per-session title overrides. The operator renames any row from
|
|
2
|
+
// the sidebar / detail pane; the title persists across manager restarts and
|
|
3
|
+
// across `--resume` PTY swaps (the canonical sessionId stays constant).
|
|
4
|
+
//
|
|
5
|
+
// Lifecycle:
|
|
6
|
+
// - Boot reads `<accountDir>/session-titles.json` into the in-memory Map.
|
|
7
|
+
// - `set(sessionId, title)` writes the map and atomically persists.
|
|
8
|
+
// - `remove(sessionId)` mutates on session purge / delete cascade.
|
|
9
|
+
// - File corruption / missing parent dir / permission errors at boot are
|
|
10
|
+
// logged and treated as empty map; the manager NEVER refuses to start.
|
|
11
|
+
//
|
|
12
|
+
// File format (UTF-8 JSON):
|
|
13
|
+
// { "<sessionId>": { "title": "...", "renamedAt": "2026-05-22T18:00:00.000Z" } }
|
|
14
|
+
//
|
|
15
|
+
// Validation lives here (single source of truth). The /rename HTTP route
|
|
16
|
+
// imports `validateUserTitle`; the UI does length-only checks for ergonomics
|
|
17
|
+
// (the binding gate is the manager route — see SKILL: writer/reader char-set
|
|
18
|
+
// drift learning).
|
|
19
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
20
|
+
const MAX_TITLE_LENGTH = 200;
|
|
21
|
+
const CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
|
|
22
|
+
/** Single source of truth for user-title validation. Mirrored in
|
|
23
|
+
* Sidebar.tsx + SessionMetadataPane.tsx as length-only client-side checks;
|
|
24
|
+
* the control-char rejection is server-only. */
|
|
25
|
+
export function validateUserTitle(raw) {
|
|
26
|
+
if (typeof raw !== 'string')
|
|
27
|
+
return { ok: false, reason: 'empty' };
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (trimmed.length === 0)
|
|
30
|
+
return { ok: false, reason: 'empty' };
|
|
31
|
+
if (trimmed.length > MAX_TITLE_LENGTH)
|
|
32
|
+
return { ok: false, reason: 'too-long' };
|
|
33
|
+
if (CONTROL_CHAR_RE.test(trimmed))
|
|
34
|
+
return { ok: false, reason: 'control-chars' };
|
|
35
|
+
return { ok: true, title: trimmed };
|
|
36
|
+
}
|
|
37
|
+
export class UserTitleStore {
|
|
38
|
+
bySessionId = new Map();
|
|
39
|
+
filePath;
|
|
40
|
+
logger;
|
|
41
|
+
loaded = false;
|
|
42
|
+
constructor(filePath, logger) {
|
|
43
|
+
this.filePath = filePath;
|
|
44
|
+
this.logger = logger;
|
|
45
|
+
}
|
|
46
|
+
/** Read the persisted file into the in-memory map. Boot-resilient: any
|
|
47
|
+
* failure (missing file, malformed JSON, permission denied) is logged
|
|
48
|
+
* and the store starts empty. Never throws. */
|
|
49
|
+
loadFromDisk() {
|
|
50
|
+
this.loaded = true;
|
|
51
|
+
if (!existsSync(this.filePath)) {
|
|
52
|
+
this.logger(`[claude-session-manager] user-title-store-load empty reason=file-missing path=${this.filePath}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = readFileSync(this.filePath, 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
61
|
+
this.logger(`[claude-session-manager] user-title-store-load failed reason=read-error detail=${detail.slice(0, 200)} — starting empty`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
70
|
+
this.logger(`[claude-session-manager] user-title-store-load failed reason=malformed-json detail=${detail.slice(0, 200)} — starting empty`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
74
|
+
this.logger(`[claude-session-manager] user-title-store-load failed reason=not-an-object — starting empty`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let loaded = 0;
|
|
78
|
+
for (const [sessionId, value] of Object.entries(parsed)) {
|
|
79
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0)
|
|
80
|
+
continue;
|
|
81
|
+
if (!value || typeof value !== 'object')
|
|
82
|
+
continue;
|
|
83
|
+
const v = value;
|
|
84
|
+
if (typeof v.title !== 'string' || v.title.trim().length === 0)
|
|
85
|
+
continue;
|
|
86
|
+
const trimmed = v.title.trim();
|
|
87
|
+
if (trimmed.length > MAX_TITLE_LENGTH)
|
|
88
|
+
continue;
|
|
89
|
+
if (CONTROL_CHAR_RE.test(trimmed))
|
|
90
|
+
continue;
|
|
91
|
+
const renamedAt = typeof v.renamedAt === 'string' && v.renamedAt.length > 0 ? v.renamedAt : new Date(0).toISOString();
|
|
92
|
+
this.bySessionId.set(sessionId, { title: trimmed, renamedAt });
|
|
93
|
+
loaded += 1;
|
|
94
|
+
}
|
|
95
|
+
this.logger(`[claude-session-manager] user-title-store-load ok entries=${loaded} path=${this.filePath}`);
|
|
96
|
+
}
|
|
97
|
+
get(sessionId) {
|
|
98
|
+
return this.bySessionId.get(sessionId);
|
|
99
|
+
}
|
|
100
|
+
/** Persist `title` for `sessionId`. Caller is responsible for validating
|
|
101
|
+
* via `validateUserTitle` first; the store stores whatever it's given. */
|
|
102
|
+
set(sessionId, title) {
|
|
103
|
+
const entry = { title, renamedAt: new Date().toISOString() };
|
|
104
|
+
this.bySessionId.set(sessionId, entry);
|
|
105
|
+
this.persistAtomic();
|
|
106
|
+
return entry;
|
|
107
|
+
}
|
|
108
|
+
/** Remove the entry for `sessionId` if present. Called on session
|
|
109
|
+
* purge/delete cascade so the file doesn't accrete orphans.
|
|
110
|
+
* Returns true when an entry was removed. */
|
|
111
|
+
remove(sessionId) {
|
|
112
|
+
const had = this.bySessionId.delete(sessionId);
|
|
113
|
+
if (had)
|
|
114
|
+
this.persistAtomic();
|
|
115
|
+
return had;
|
|
116
|
+
}
|
|
117
|
+
/** Current in-memory snapshot — diagnostic only. */
|
|
118
|
+
size() {
|
|
119
|
+
return this.bySessionId.size;
|
|
120
|
+
}
|
|
121
|
+
persistAtomic() {
|
|
122
|
+
const obj = {};
|
|
123
|
+
for (const [sessionId, entry] of this.bySessionId)
|
|
124
|
+
obj[sessionId] = entry;
|
|
125
|
+
const tmp = `${this.filePath}.tmp`;
|
|
126
|
+
try {
|
|
127
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2), 'utf-8');
|
|
128
|
+
renameSync(tmp, this.filePath);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
132
|
+
this.logger(`[claude-session-manager] user-title-store-persist failed detail=${detail.slice(0, 200)}`);
|
|
133
|
+
// No rethrow — a rename whose persist failed still serves reads from
|
|
134
|
+
// the in-memory map until next boot. Boot-resilient on next start.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=user-title-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-title-store.js","sourceRoot":"","sources":["../src/user-title-store.ts"],"names":[],"mappings":"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"}
|
package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md
CHANGED
|
@@ -176,7 +176,10 @@ When any field above is unresolved after consulting its primary source, **stop b
|
|
|
176
176
|
- **Web bundle ships the PDF, not per-page JPGs.** The bundle does not include `cover-print.jpg … backpage-print.jpg` — those duplicate the brochure content already embedded in `<slug>-brochure.pdf`. The web copy of `brochure.html` has its `.print-img` src attributes cleared so the print stylesheet drops to the live-DOM fallback; operators who want a printable PDF download the bundled PDF directly. See `references/build.md → Clear .print-img src in the web copy`. Shipping both was the historical bundle-bloat defect.
|
|
177
177
|
- **Pages with absolutely-positioned bottom elements must reserve their footprint in the in-flow content's padding.** Two absolute elements live at the bottom of every `.page`: the `.folio` page-number strip (`bottom: 8mm`, ~8mm tall → occupies bottom ~16mm) and, on the back page only, the `.back-disclaimer` (`bottom: 16mm`, wraps to up to 5 lines × 3.7mm → occupies bottom ~34.5mm). In-flow content above them (`.mi-foot` with `margin-top: auto` inside `.mi-page`, or `.backpage-inner` content with `justify-content: center`) is a SIBLING of those absolute elements — nothing in CSS structure enforces clearance. The fix is always to pull the in-flow container UP via its parent's `padding-bottom`: `.mi-page` uses **22mm** (folio clearance), `.backpage-inner` uses **42mm** (folio + disclaimer clearance with 4mm gap). Any future page that adds an absolute bottom element must update the in-flow parent's `padding-bottom` accordingly — never try to push the absolute element down with `margin-*` (no positional effect on `position: absolute` whose anchor is `bottom`). This is the "Absolute vs in-flow" lesson from the recommendations doc — pull the in-flow element, never the absolute one.
|
|
178
178
|
- **Folio is plain text on whitespace or absent — never chrome over imagery.** The `.folio > span` carries no background, blur, padding, border-radius, or shadow. It is plain text in `--teal-900` that sits on the clean whitespace reserved at the page bottom (see the previous rule on padding-bottom clearance). The folio is **hidden entirely** on full-bleed-bottom pages: `.cover`, `.page.intermission`, `.page.folio-dark`, and any `.page` whose body wraps a `.feature-page` (photo+panel layout where the photo bleeds to the page bottom). Never reintroduce a folio-pill background to "make it readable over the image" — that's competing chrome on the page; suppress the folio on that page instead. Any new full-bleed-bottom page class must be added to the `.folio` `display: none` selector in `references/template.html`.
|
|
179
|
+
- **Page 13's `{{ siteplan_src }}` is never the floorplan.** Page 13 (the site-plan / plot page, `.page siteplan-page`, "Continued, Chapter VI · The plot") expects a plot drawing, site plan, or aerial photo — an image distinct from the floorplan that page 14 already carries. If no such image is naturally available (no `<slug>-siteplan.png` line-art, no aerial photo on disk), the operator confirmation prompt MUST list `{{ siteplan_src }}` as an unresolved field and request a substitute by name — a wide exterior, a paddock/grounds photo, a drone shot of the property in its setting, or a hand-marked site sketch. Silently falling back to the floorplan duplicates page 14's content and is a defect (the brochure ships with the same image on two consecutive pages — visible immediately on review). The fallback is forbidden by the same doctrine as `TBC`: never coerce a field to a default the operator did not confirm. If the operator cannot supply a substitute, the brochure drops to the 12-page reduced layout (no page 13) — not to a 16-page layout with a duplicated floorplan.
|
|
179
180
|
- **Verification covers every page in the deliverable — sampling is a defect, not a shortcut.** `scrollHeight` fit checks, computed-style collision probes (`.mi-foot` ↔ `.folio` gap, `.back-disclaimer` ↔ in-flow content, any future absolute-bottom element vs in-flow neighbour), and visual review before declaring done all walk **every page** in the 16-page folio (or 12-page reduced layout). "Sample-checked 3 of 12 pages" or any variant ("spot-checked the spreads I changed", "skimmed a few representative pages") is not authorised — the skill explicitly says "verify every page" in `references/build.md → Live editing workflow` step 8 and in `references/structure.md`. The deliverable is the brochure as a whole; a defect on an unchecked page ships exactly as readily as a defect on a checked one. If a probe is expensive enough that walking 16 pages feels burdensome, the cost is the probe's, not the skill's — propose a faster probe rather than narrowing the coverage.
|
|
181
|
+
- **`scrollHeight > viewport budget` is ALWAYS a defect — never "negligible".** Landscape budget is **794px** (`min-height: 210mm` at 96dpi); portrait is **1123px**. Every `.page`'s `scrollHeight` must be `≤` the budget — strict, not "approximately equal". A 4px overflow IS a defect: `overflow: hidden` silently clips that excess, and on pages with absolute-positioned bottom chrome (`.folio`, `.back-disclaimer`) the clipped content paints over chrome that lives in the absolute layer. "It's only 4 pixels" is the rationalisation that ships defects — the rule does not have a tolerance band. If a page reports `scrollHeight > budget` even by 1px, the build halts until the operator addresses the content-volume mismatch (trim copy, shrink an image, reduce a row count, or split into a continuation page). Never paper over with `overflow: hidden`-as-clip; the warning sign is the deliverable's only signal that the content doesn't fit.
|
|
182
|
+
- **Material Information table has a hard row capacity — overflow is a content-volume defect, not a render bug.** At the default `.mi-row` sizing (9.5/11.5px fonts, 6px vertical padding, 18px `.mi-grid` row-gap), one column in the three-column MI grid fits **~13 rows** within the 22mm-clearance budget. A property with an unusually full table (e.g. 17+ rows in the tallest column, observed on a recent run) overruns by ~25mm / ~85px and clips into the folio zone. When MI's content volume exceeds the budget, the operator confirmation prompt MUST surface `mi.<column>.row_count` as an overflow item and ask the operator to either (a) trim the column to ≤ 13 rows by consolidating related disclosures (e.g. merge "easements" + "water/sewerage" + "rights of way" into one "Other matters" row that points at the seller's solicitor), or (b) opt into a continuation page (15a / 15b) — a separate skill-level decision that breaks the default 16-page contract and must be operator-confirmed. The build never silently truncates rows, never shrinks fonts to "make it fit", and never accepts a scrollHeight overflow as "negligible". The default `.mi-page` 22mm bottom-padding is sized for the typical property; properties outside the typical range MUST resolve via operator confirmation, not via the agent's judgement about which rows are "less important".
|
|
180
183
|
- **No `{{ token }}` may remain in rendered output** — `grep '{{' output/brochure.html` must return zero matches before PDFs are built.
|
|
181
184
|
|
|
182
185
|
## Scope
|