@rubytech/create-maxy-code 0.1.92 → 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.
Files changed (108) hide show
  1. package/dist/__tests__/claude-ptys-slice.test.js +25 -0
  2. package/dist/index.js +10 -1
  3. package/dist/port-resolution.js +29 -0
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/docs/references/platform.md +4 -2
  6. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +61 -2
  7. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  8. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +337 -32
  9. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  10. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +1 -3
  11. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  12. package/payload/platform/services/claude-session-manager/dist/http-server.js +307 -209
  13. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  14. package/payload/platform/services/claude-session-manager/dist/index.js +62 -13
  15. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts +9 -0
  17. package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/jsonl-path.js +14 -0
  19. package/payload/platform/services/claude-session-manager/dist/jsonl-path.js.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +165 -33
  21. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +283 -92
  23. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts +4 -2
  25. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
  26. package/payload/platform/services/claude-session-manager/dist/reaper.js +23 -22
  27. package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +55 -0
  29. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -0
  30. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +170 -0
  31. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -0
  32. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +114 -0
  33. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -0
  34. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +209 -0
  35. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -0
  36. package/payload/server/public/assets/{ChatInput-dLWJ0Q8I.css → ChatInput-CzQhPt90.css} +1 -1
  37. package/payload/server/public/assets/{Checkbox-Bx8mRHQ3.js → Checkbox-ynZtDWi3.js} +1 -1
  38. package/payload/server/public/assets/{admin-BhEvxMrC.js → admin-ux6TKHS1.js} +1 -1
  39. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BEW_bD50.js → architectureDiagram-Q4EWVU46-Ko_izKmY.js} +1 -1
  40. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DFLT98C3.js → blockDiagram-DXYQGD6D-D58VDC0w.js} +1 -1
  41. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-BemKtROA.js → c4Diagram-AHTNJAMY-C-eRXyKi.js} +1 -1
  42. package/payload/server/public/assets/channel-CejaITMF.js +1 -0
  43. package/payload/server/public/assets/{chunk-336JU56O-BL8fGJ7_.js → chunk-336JU56O-DZLEfLAb.js} +2 -2
  44. package/payload/server/public/assets/{chunk-426QAEUC-BD6by0MP.js → chunk-426QAEUC-BriTU3L8.js} +1 -1
  45. package/payload/server/public/assets/{chunk-4TB4RGXK-DX-cb6Nd.js → chunk-4TB4RGXK-BdxRL-AY.js} +1 -1
  46. package/payload/server/public/assets/{chunk-5FUZZQ4R-B2QqtIZS.js → chunk-5FUZZQ4R-BgEmy7Y3.js} +1 -1
  47. package/payload/server/public/assets/{chunk-5PVQY5BW-CzJWmsmd.js → chunk-5PVQY5BW-emnbDRLA.js} +1 -1
  48. package/payload/server/public/assets/{chunk-EDXVE4YY-DP9vcvEU.js → chunk-EDXVE4YY-BvCf2Xf2.js} +1 -1
  49. package/payload/server/public/assets/{chunk-ENJZ2VHE-D5xWEjF0.js → chunk-ENJZ2VHE-DTpEYJRb.js} +1 -1
  50. package/payload/server/public/assets/{chunk-ICPOFSXX-AKduf9o4.js → chunk-ICPOFSXX-Ced2j48Y.js} +1 -1
  51. package/payload/server/public/assets/{chunk-OYMX7WX6-D6jsVNyQ.js → chunk-OYMX7WX6-B21_a3Dv.js} +1 -1
  52. package/payload/server/public/assets/{chunk-U2HBQHQK-BNAnTYh8.js → chunk-U2HBQHQK-CPS3yyXK.js} +1 -1
  53. package/payload/server/public/assets/{chunk-X2U36JSP-UdQhYDnh.js → chunk-X2U36JSP-BJ_B5NW7.js} +1 -1
  54. package/payload/server/public/assets/{chunk-YZCP3GAM-DSSmSGmS.js → chunk-YZCP3GAM-CMLy-WXm.js} +1 -1
  55. package/payload/server/public/assets/{chunk-ZZ45TVLE-Czm7IDMC.js → chunk-ZZ45TVLE-BkLnzCHe.js} +1 -1
  56. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BmoqALf4.js +1 -0
  57. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-vMajwCNi.js +1 -0
  58. package/payload/server/public/assets/clone-mvVfHkyg.js +1 -0
  59. package/payload/server/public/assets/{dagre-hSIMHJcO.js → dagre-Dd_gq4yj.js} +1 -1
  60. package/payload/server/public/assets/{dagre-KV5264BT-CBuRCJE7.js → dagre-KV5264BT-RY0E5UAn.js} +1 -1
  61. package/payload/server/public/assets/data-BK4r8Dqy.js +1 -0
  62. package/payload/server/public/assets/{diagram-5BDNPKRD-dbMrH0le.js → diagram-5BDNPKRD-DR7XiPLh.js} +1 -1
  63. package/payload/server/public/assets/{diagram-G4DWMVQ6-DBc6hs9a.js → diagram-G4DWMVQ6-2EbOfKFs.js} +1 -1
  64. package/payload/server/public/assets/{diagram-MMDJMWI5-ubSGybUk.js → diagram-MMDJMWI5-CGcY0Ze7.js} +1 -1
  65. package/payload/server/public/assets/{diagram-TYMM5635-WxL3cv-9.js → diagram-TYMM5635-Dk6MmAso.js} +1 -1
  66. package/payload/server/public/assets/{erDiagram-SMLLAGMA-CrBBOAN8.js → erDiagram-SMLLAGMA-CKvdlUqk.js} +1 -1
  67. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CXUq5Hzt.js → flowDiagram-DWJPFMVM-BqKhvCDz.js} +1 -1
  68. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-vTCBnYVS.js → ganttDiagram-T4ZO3ILL-CeiE-IRb.js} +1 -1
  69. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BHrj_bRa.js → gitGraphDiagram-UUTBAWPF-BbPKn7v2.js} +1 -1
  70. package/payload/server/public/assets/graph-Cb6A8Hhs.js +1 -0
  71. package/payload/server/public/assets/graph-labels-DCsUNtnE.js +1 -0
  72. package/payload/server/public/assets/{graphlib-CDO75l2X.js → graphlib-tG6kR59B.js} +1 -1
  73. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CcQdaOhi.js → infoDiagram-42DDH7IO-6MiRGCI1.js} +1 -1
  74. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-3KvltFgV.js → ishikawaDiagram-UXIWVN3A-BwY4p5mg.js} +1 -1
  75. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DZCXaGFE.js → journeyDiagram-VCZTEJTY-BXAQ_fUi.js} +1 -1
  76. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DAIAKy1-.js → kanban-definition-6JOO6SKY-ChiiO7Yt.js} +1 -1
  77. package/payload/server/public/assets/{lib-CCLURO3E.js → lib-DbPz9cfu.js} +1 -1
  78. package/payload/server/public/assets/{line-C8iCE2Wx.js → line-B5g5OrOD.js} +1 -1
  79. package/payload/server/public/assets/{mermaid-parser.core-ElvUhnV9.js → mermaid-parser.core-BMGzMKJx.js} +1 -1
  80. package/payload/server/public/assets/{mermaid.core-7dSH31Ao.js → mermaid.core-DLMgkjXn.js} +3 -3
  81. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-FZ0rcfac.js → mindmap-definition-QFDTVHPH-BvyzRLAi.js} +1 -1
  82. package/payload/server/public/assets/{page-C1hj89Gf.js → page-BeYR_GI2.js} +1 -1
  83. package/payload/server/public/assets/{page-Bu8kO4jO.js → page-DVNp-Dfy.js} +1 -1
  84. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CDuvMZbO.js → pieDiagram-DEJITSTG--EV9pVfs.js} +1 -1
  85. package/payload/server/public/assets/{public-g_2skbnY.js → public-sYW0Dwku.js} +3 -3
  86. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-xDjhkqNc.js → quadrantDiagram-34T5L4WZ-DJcL08ft.js} +1 -1
  87. package/payload/server/public/assets/{requirementDiagram-MS252O5E-BPKpyT0n.js → requirementDiagram-MS252O5E-D4VdmmSu.js} +1 -1
  88. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BvFIrihC.js → sankeyDiagram-XADWPNL6-Dsw3nHRS.js} +1 -1
  89. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-C6G1oAE0.js → sequenceDiagram-FGHM5R23-CAgemUx5.js} +1 -1
  90. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DbOJT_Xh.js → stateDiagram-FHFEXIEX-B7dyL9_X.js} +1 -1
  91. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CImN6Wk6.js +1 -0
  92. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D1DSxHh6.js → timeline-definition-GMOUNBTQ-B7iKswhp.js} +1 -1
  93. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BYoSeMdo.js → vennDiagram-DHZGUBPP-uFS0i1qX.js} +1 -1
  94. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-DH3kZdea.js → wardleyDiagram-NUSXRM2D-lDUh4LAD.js} +1 -1
  95. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-C6MROMLG.js → xychartDiagram-5P7HB3ND-0v7MEVwW.js} +1 -1
  96. package/payload/server/public/data.html +5 -5
  97. package/payload/server/public/graph.html +6 -6
  98. package/payload/server/public/index.html +8 -8
  99. package/payload/server/public/public.html +5 -5
  100. package/payload/server/public/assets/channel-CfpByn-m.js +0 -1
  101. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DNc9R1IX.js +0 -1
  102. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DM1DQh8N.js +0 -1
  103. package/payload/server/public/assets/clone-BSrNdvLW.js +0 -1
  104. package/payload/server/public/assets/data-Cbo0tTS3.js +0 -1
  105. package/payload/server/public/assets/graph-Beucu3_J.js +0 -1
  106. package/payload/server/public/assets/graph-labels-D15-eFQK.js +0 -1
  107. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ce4GFk-H.js +0 -1
  108. /package/payload/server/public/assets/{ChatInput-DoS80kaC.js → ChatInput-Cj9JQtYt.js} +0 -0
@@ -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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"}