@cleocode/contracts 2026.5.130 → 2026.5.131

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 (171) hide show
  1. package/dist/__tests__/atomic-tools-contract.test.d.ts +14 -0
  2. package/dist/__tests__/atomic-tools-contract.test.d.ts.map +1 -0
  3. package/dist/__tests__/atomic-tools-contract.test.js +55 -0
  4. package/dist/__tests__/atomic-tools-contract.test.js.map +1 -0
  5. package/dist/__tests__/gateway-contract.test.d.ts +18 -0
  6. package/dist/__tests__/gateway-contract.test.d.ts.map +1 -0
  7. package/dist/__tests__/gateway-contract.test.js +187 -0
  8. package/dist/__tests__/gateway-contract.test.js.map +1 -0
  9. package/dist/__tests__/mvi.test.d.ts +17 -0
  10. package/dist/__tests__/mvi.test.d.ts.map +1 -0
  11. package/dist/__tests__/mvi.test.js +147 -0
  12. package/dist/__tests__/mvi.test.js.map +1 -0
  13. package/dist/__tests__/operation-def.test.js +1 -1
  14. package/dist/__tests__/operation-def.test.js.map +1 -1
  15. package/dist/boundary.d.ts +26 -0
  16. package/dist/boundary.d.ts.map +1 -1
  17. package/dist/boundary.js +42 -4
  18. package/dist/boundary.js.map +1 -1
  19. package/dist/branch-lock.d.ts +35 -1
  20. package/dist/branch-lock.d.ts.map +1 -1
  21. package/dist/branch-lock.js +23 -2
  22. package/dist/branch-lock.js.map +1 -1
  23. package/dist/daemon/__tests__/health-roundtrip.test.d.ts +13 -0
  24. package/dist/daemon/__tests__/health-roundtrip.test.d.ts.map +1 -0
  25. package/dist/daemon/__tests__/health-roundtrip.test.js +69 -0
  26. package/dist/daemon/__tests__/health-roundtrip.test.js.map +1 -0
  27. package/dist/daemon/health.d.ts +108 -0
  28. package/dist/daemon/health.d.ts.map +1 -0
  29. package/dist/daemon/health.js +95 -0
  30. package/dist/daemon/health.js.map +1 -0
  31. package/dist/daemon/index.d.ts +24 -0
  32. package/dist/daemon/index.d.ts.map +1 -0
  33. package/dist/daemon/index.js +22 -0
  34. package/dist/daemon/index.js.map +1 -0
  35. package/dist/daemon/subsystem.d.ts +121 -0
  36. package/dist/daemon/subsystem.d.ts.map +1 -0
  37. package/dist/daemon/subsystem.js +28 -0
  38. package/dist/daemon/subsystem.js.map +1 -0
  39. package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.d.ts +15 -0
  40. package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.d.ts.map +1 -0
  41. package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.js +40 -0
  42. package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.js.map +1 -0
  43. package/dist/daemon-ipc/index.d.ts +44 -0
  44. package/dist/daemon-ipc/index.d.ts.map +1 -0
  45. package/dist/daemon-ipc/index.js +43 -0
  46. package/dist/daemon-ipc/index.js.map +1 -0
  47. package/dist/db-inventory.json +8 -8
  48. package/dist/dispatch/identity.d.ts +1 -1
  49. package/dist/dispatch/identity.d.ts.map +1 -1
  50. package/dist/dispatch/identity.js +3 -0
  51. package/dist/dispatch/identity.js.map +1 -1
  52. package/dist/dispatch/operation-def.d.ts +16 -0
  53. package/dist/dispatch/operation-def.d.ts.map +1 -1
  54. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  55. package/dist/dispatch/operations-registry.js +133 -0
  56. package/dist/dispatch/operations-registry.js.map +1 -1
  57. package/dist/exit-codes.d.ts +1 -1
  58. package/dist/exit-codes.js +1 -1
  59. package/dist/gateway/rpc/__tests__/freeze.test.d.ts +20 -0
  60. package/dist/gateway/rpc/__tests__/freeze.test.d.ts.map +1 -0
  61. package/dist/gateway/rpc/__tests__/freeze.test.js +122 -0
  62. package/dist/gateway/rpc/__tests__/freeze.test.js.map +1 -0
  63. package/dist/gateway/rpc/index.d.ts +23 -0
  64. package/dist/gateway/rpc/index.d.ts.map +1 -0
  65. package/dist/gateway/rpc/index.js +22 -0
  66. package/dist/gateway/rpc/index.js.map +1 -0
  67. package/dist/gateway/rpc/messages.d.ts +263 -0
  68. package/dist/gateway/rpc/messages.d.ts.map +1 -0
  69. package/dist/gateway/rpc/messages.js +152 -0
  70. package/dist/gateway/rpc/messages.js.map +1 -0
  71. package/dist/gateway/rpc/version.d.ts +39 -0
  72. package/dist/gateway/rpc/version.d.ts.map +1 -0
  73. package/dist/gateway/rpc/version.js +39 -0
  74. package/dist/gateway/rpc/version.js.map +1 -0
  75. package/dist/gateway.d.ts +239 -0
  76. package/dist/gateway.d.ts.map +1 -0
  77. package/dist/gateway.js +171 -0
  78. package/dist/gateway.js.map +1 -0
  79. package/dist/goal.d.ts +314 -0
  80. package/dist/goal.d.ts.map +1 -0
  81. package/dist/goal.js +118 -0
  82. package/dist/goal.js.map +1 -0
  83. package/dist/index.d.ts +9 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +10 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/mvi.d.ts +209 -0
  88. package/dist/mvi.d.ts.map +1 -0
  89. package/dist/mvi.js +99 -0
  90. package/dist/mvi.js.map +1 -0
  91. package/dist/operations/attention.d.ts +114 -0
  92. package/dist/operations/attention.d.ts.map +1 -0
  93. package/dist/operations/attention.js +22 -0
  94. package/dist/operations/attention.js.map +1 -0
  95. package/dist/operations/focus.d.ts +25 -0
  96. package/dist/operations/focus.d.ts.map +1 -1
  97. package/dist/operations/index.d.ts +1 -0
  98. package/dist/operations/index.d.ts.map +1 -1
  99. package/dist/operations/index.js +1 -0
  100. package/dist/operations/index.js.map +1 -1
  101. package/dist/operations/session.d.ts +11 -0
  102. package/dist/operations/session.d.ts.map +1 -1
  103. package/dist/sdk-tool.d.ts +1 -1
  104. package/dist/sdk-tool.js +1 -1
  105. package/dist/supervisor-ipc/__tests__/freeze.test.d.ts +15 -0
  106. package/dist/supervisor-ipc/__tests__/freeze.test.d.ts.map +1 -0
  107. package/dist/supervisor-ipc/__tests__/freeze.test.js +159 -0
  108. package/dist/supervisor-ipc/__tests__/freeze.test.js.map +1 -0
  109. package/dist/supervisor-ipc/index.d.ts +15 -0
  110. package/dist/supervisor-ipc/index.d.ts.map +1 -0
  111. package/dist/supervisor-ipc/index.js +14 -0
  112. package/dist/supervisor-ipc/index.js.map +1 -0
  113. package/dist/supervisor-ipc/messages.d.ts +457 -0
  114. package/dist/supervisor-ipc/messages.d.ts.map +1 -0
  115. package/dist/supervisor-ipc/messages.js +312 -0
  116. package/dist/supervisor-ipc/messages.js.map +1 -0
  117. package/dist/supervisor-ipc/version.d.ts +29 -0
  118. package/dist/supervisor-ipc/version.d.ts.map +1 -0
  119. package/dist/supervisor-ipc/version.js +29 -0
  120. package/dist/supervisor-ipc/version.js.map +1 -0
  121. package/dist/tools/__tests__/composite.test.d.ts +14 -0
  122. package/dist/tools/__tests__/composite.test.d.ts.map +1 -0
  123. package/dist/tools/__tests__/composite.test.js +75 -0
  124. package/dist/tools/__tests__/composite.test.js.map +1 -0
  125. package/dist/tools/atomic.d.ts +195 -0
  126. package/dist/tools/atomic.d.ts.map +1 -0
  127. package/dist/tools/atomic.js +82 -0
  128. package/dist/tools/atomic.js.map +1 -0
  129. package/dist/tools/composite.d.ts +128 -0
  130. package/dist/tools/composite.d.ts.map +1 -0
  131. package/dist/tools/composite.js +88 -0
  132. package/dist/tools/composite.js.map +1 -0
  133. package/package.json +34 -2
  134. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  135. package/src/__tests__/atomic-tools-contract.test.ts +67 -0
  136. package/src/__tests__/gateway-contract.test.ts +219 -0
  137. package/src/__tests__/mvi.test.ts +175 -0
  138. package/src/__tests__/operation-def.test.ts +2 -1
  139. package/src/boundary.ts +74 -4
  140. package/src/branch-lock.ts +49 -2
  141. package/src/daemon/__tests__/health-roundtrip.test.ts +84 -0
  142. package/src/daemon/health.ts +121 -0
  143. package/src/daemon/index.ts +35 -0
  144. package/src/daemon/subsystem.ts +126 -0
  145. package/src/daemon-ipc/__tests__/daemon-ipc-freeze.test.ts +53 -0
  146. package/src/daemon-ipc/index.ts +90 -0
  147. package/src/db-inventory.json +8 -8
  148. package/src/dispatch/identity.ts +3 -0
  149. package/src/dispatch/operation-def.ts +16 -0
  150. package/src/dispatch/operations-registry.ts +140 -0
  151. package/src/exit-codes.ts +1 -1
  152. package/src/gateway/rpc/__tests__/freeze.test.ts +139 -0
  153. package/src/gateway/rpc/index.ts +43 -0
  154. package/src/gateway/rpc/messages.ts +187 -0
  155. package/src/gateway/rpc/version.ts +40 -0
  156. package/src/gateway.ts +252 -0
  157. package/src/goal.ts +348 -0
  158. package/src/index.ts +109 -0
  159. package/src/mvi.ts +236 -0
  160. package/src/operations/attention.ts +117 -0
  161. package/src/operations/focus.ts +26 -0
  162. package/src/operations/index.ts +1 -0
  163. package/src/operations/session.ts +11 -0
  164. package/src/sdk-tool.ts +1 -1
  165. package/src/supervisor-ipc/__tests__/freeze.test.ts +180 -0
  166. package/src/supervisor-ipc/index.ts +62 -0
  167. package/src/supervisor-ipc/messages.ts +396 -0
  168. package/src/supervisor-ipc/version.ts +30 -0
  169. package/src/tools/__tests__/composite.test.ts +88 -0
  170. package/src/tools/atomic.ts +274 -0
  171. package/src/tools/composite.ts +173 -0
@@ -309,13 +309,23 @@ export type BranchLockErrorCode =
309
309
  * Exported as a frozen const tuple so git-shim, harness adapters, and test
310
310
  * utilities can reference the authoritative key list without duplication.
311
311
  *
312
+ * `CLEO_SESSION_ID` + `CLEO_AGENT_ID` were added in T11343 (Epic T11284,
313
+ * SG-COGNITIVE-SUBSTRATE) so every spawned agent carries its OWN session and
314
+ * agent identity into the isolation shell. Before this, spawn injected only
315
+ * worktree-binding keys and every short-lived `cleo` call inside the worktree
316
+ * collapsed onto "whoever touched the DB last" via `getActiveSession()` —
317
+ * the root cause of multi-agent session-bleed AND memory scope-leakage.
318
+ *
312
319
  * @task T1759
320
+ * @task T11343
313
321
  */
314
322
  export const ISOLATION_ENV_KEYS = [
315
323
  'CLEO_WORKTREE_ROOT',
316
324
  'CLEO_AGENT_ROLE',
317
325
  'CLEO_WORKTREE_BRANCH',
318
326
  'CLEO_PROJECT_HASH',
327
+ 'CLEO_SESSION_ID',
328
+ 'CLEO_AGENT_ID',
319
329
  ] as const satisfies readonly string[];
320
330
 
321
331
  /** Union of all isolation env key strings. */
@@ -402,6 +412,32 @@ export interface IsolationOptions {
402
412
  role: 'worker' | 'orchestrator';
403
413
  /** Project hash scoping this worktree under the XDG root (sha256(projectRoot)[:16]). */
404
414
  projectHash: string;
415
+ /**
416
+ * Per-agent CLEO session id to bind to the isolation shell (T11343).
417
+ *
418
+ * Injected as `CLEO_SESSION_ID` so every short-lived `cleo` call inside the
419
+ * worktree resolves THIS agent's session via `resolveSessionIdFromEnv()`
420
+ * rather than collapsing onto the orchestrator's most-recent active row.
421
+ * When omitted, `CLEO_SESSION_ID` is set to the empty string and downstream
422
+ * resolution falls back to the legacy `getActiveSession()` path (backward
423
+ * compatible — no behaviour change for callers that do not yet allocate a
424
+ * per-agent session).
425
+ *
426
+ * @defaultValue '' (empty — falls back to active-session resolution)
427
+ * @task T11343
428
+ */
429
+ sessionId?: string;
430
+ /**
431
+ * Per-agent identity handle to bind to the isolation shell (T11343).
432
+ *
433
+ * Injected as `CLEO_AGENT_ID` so audit rows, conduit messages, and memory
434
+ * observations are attributed to the spawned agent rather than to the
435
+ * orchestrator. When omitted, `CLEO_AGENT_ID` is set to the empty string.
436
+ *
437
+ * @defaultValue '' (empty — no per-agent attribution)
438
+ * @task T11343
439
+ */
440
+ agentId?: string;
405
441
  }
406
442
 
407
443
  /**
@@ -468,17 +504,23 @@ export interface IsolationResult {
468
504
  * @task T1759
469
505
  */
470
506
  export function provisionIsolatedShell(options: IsolationOptions): IsolationResult {
471
- const { worktreePath, branch, role, projectHash } = options;
507
+ const { worktreePath, branch, role, projectHash, sessionId, agentId } = options;
472
508
 
473
509
  // --- cwd -----------------------------------------------------------------
474
510
  const cwd = worktreePath;
475
511
 
476
512
  // --- env -----------------------------------------------------------------
513
+ // T11343: CLEO_SESSION_ID + CLEO_AGENT_ID bind the spawned agent's own
514
+ // identity into the shell. Empty-string default keeps backward compatibility
515
+ // — `resolveSessionIdFromEnv()` treats '' as absent and falls back to the
516
+ // legacy active-session resolution path.
477
517
  const env: Record<(typeof ISOLATION_ENV_KEYS)[number], string> = {
478
518
  CLEO_WORKTREE_ROOT: worktreePath,
479
519
  CLEO_AGENT_ROLE: role,
480
520
  CLEO_WORKTREE_BRANCH: branch,
481
521
  CLEO_PROJECT_HASH: projectHash,
522
+ CLEO_SESSION_ID: sessionId ?? '',
523
+ CLEO_AGENT_ID: agentId ?? '',
482
524
  };
483
525
 
484
526
  // --- preamble ------------------------------------------------------------
@@ -487,7 +529,12 @@ export function provisionIsolatedShell(options: IsolationOptions): IsolationResu
487
529
  // - The worktree directory having been pruned before the agent starts.
488
530
  // - A future change where cwd is silently coerced to something else.
489
531
  // - Symlink traversal producing a path that doesn't share the prefix.
490
- const exportBlock = ISOLATION_ENV_KEYS.map((k) => `export ${k}="${env[k]}"`).join('\n');
532
+ // T11343: skip empty values so an unallocated CLEO_SESSION_ID / CLEO_AGENT_ID
533
+ // does not emit a clobbering `export KEY=""` that would clear an inherited
534
+ // value. Worktree-binding keys are always non-empty so they always export.
535
+ const exportBlock = ISOLATION_ENV_KEYS.filter((k) => env[k] !== '')
536
+ .map((k) => `export ${k}="${env[k]}"`)
537
+ .join('\n');
491
538
 
492
539
  const preamble = [
493
540
  '## Worktree Isolation (REQUIRED — do not skip)',
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Round-trip guard for the daemon health contracts (T11366 AC5).
3
+ *
4
+ * Proves a {@link HealthStatus} aggregate projects onto the FROZEN
5
+ * `supervisor-ipc` {@link MonitorResponseSchema} shape **without a lossy cast**:
6
+ * `toMonitorChildren` yields rows that validate against the supervisor's own
7
+ * `MonitorResponse.children` schema, and a `monitor` response carrying them
8
+ * round-trips through `MonitorResponseSchema.parse`.
9
+ *
10
+ * @task T11366
11
+ */
12
+
13
+ import { describe, expect, it } from 'vitest';
14
+ import {
15
+ type HealthStatus,
16
+ MonitorResponseSchema,
17
+ type SubsystemHealth,
18
+ SubsystemHealthSchema,
19
+ summarizeHealth,
20
+ toMonitorChildren,
21
+ } from '../../index.js';
22
+
23
+ describe('daemon HealthStatus ↔ supervisor-ipc MonitorResponse (T11366)', () => {
24
+ const rows: SubsystemHealth[] = [
25
+ { child_id: 'studio', pid: 4242, state: 'running', restart_count: 0 },
26
+ {
27
+ child_id: 'gc-cron',
28
+ pid: 0,
29
+ state: 'stopped',
30
+ restart_count: 2,
31
+ detail: 'awaiting next scheduled run',
32
+ },
33
+ ];
34
+
35
+ it('summarizeHealth computes allHealthy from row states', () => {
36
+ expect(summarizeHealth(rows).allHealthy).toBe(false);
37
+ expect(
38
+ summarizeHealth([{ child_id: 'studio', pid: 1, state: 'running', restart_count: 0 }])
39
+ .allHealthy,
40
+ ).toBe(true);
41
+ });
42
+
43
+ it('toMonitorChildren projects onto MonitorResponse.children without lossy casts', () => {
44
+ const health: HealthStatus = summarizeHealth(rows);
45
+ const children = toMonitorChildren(health);
46
+
47
+ // Each projected row must validate against the FROZEN supervisor schema.
48
+ const monitor = {
49
+ kind: 'monitor' as const,
50
+ children,
51
+ };
52
+ const parsed = MonitorResponseSchema.safeParse(monitor);
53
+ expect(parsed.success).toBe(true);
54
+ if (parsed.success) {
55
+ expect(parsed.data.children).toHaveLength(2);
56
+ // The daemon-only `detail` field is stripped on projection.
57
+ expect(parsed.data.children[1]).not.toHaveProperty('detail');
58
+ expect(parsed.data.children[0]?.child_id).toBe('studio');
59
+ }
60
+ });
61
+
62
+ it('a SubsystemHealth row is a structural superset of a supervisor ChildStatus', () => {
63
+ // Validating with the daemon schema accepts the supervisor fields verbatim.
64
+ const row = SubsystemHealthSchema.parse({
65
+ child_id: 'web',
66
+ pid: 9000,
67
+ state: 'running',
68
+ restart_count: 0,
69
+ });
70
+ expect(row.child_id).toBe('web');
71
+ // detail is optional and absent here.
72
+ expect(row.detail).toBeUndefined();
73
+ });
74
+
75
+ it('rejects a health row with a state outside the frozen ChildState enum', () => {
76
+ const bad = SubsystemHealthSchema.safeParse({
77
+ child_id: 'x',
78
+ pid: 0,
79
+ state: 'paused',
80
+ restart_count: 0,
81
+ });
82
+ expect(bad.success).toBe(false);
83
+ });
84
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Daemon health contracts — the aggregate shape a subsystem's `healthProbe`
3
+ * produces and the Rust supervisor consumes.
4
+ *
5
+ * These types **compose** the FROZEN `supervisor-ipc` v1.0 contract
6
+ * ({@link ChildState}, {@link ChildStatus}, {@link MonitorResponse}) rather than
7
+ * redeclaring it: a single subsystem health row maps onto a supervised
8
+ * {@link ChildStatus}, and a {@link HealthStatus} aggregate projects losslessly
9
+ * onto the {@link MonitorResponse} shape the supervisor already aggregates. This
10
+ * keeps the daemon submodule's probe output on the same wire vocabulary the
11
+ * supervisor monitors with — no parallel health taxonomy.
12
+ *
13
+ * @packageDocumentation
14
+ * @module @cleocode/contracts/daemon
15
+ *
16
+ * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule
17
+ * @task T11366 — daemon lifecycle + subsystem contracts
18
+ * @saga T11243 SG-RUNTIME-UNIFICATION
19
+ */
20
+
21
+ import { z } from 'zod';
22
+ import {
23
+ type ChildState,
24
+ ChildStateSchema,
25
+ type ChildStatus,
26
+ ChildStatusSchema,
27
+ } from '../supervisor-ipc/messages.js';
28
+
29
+ /**
30
+ * The liveness state of a single subsystem.
31
+ *
32
+ * This is the FROZEN `supervisor-ipc` {@link ChildState} (`running` |
33
+ * `restarting` | `stopped`) re-exported under a daemon-facing name so subsystem
34
+ * authors do not reach across into the IPC module. A subsystem maps onto a
35
+ * supervised child, so the two states are deliberately identical.
36
+ */
37
+ export type SubsystemState = ChildState;
38
+
39
+ /**
40
+ * Zod schema for {@link SubsystemState}. Aliases the FROZEN
41
+ * {@link ChildStateSchema} — additive only; never edit the underlying enum.
42
+ */
43
+ export const SubsystemStateSchema = ChildStateSchema;
44
+
45
+ /**
46
+ * A single subsystem's health row.
47
+ *
48
+ * Structurally a superset of the supervisor's {@link ChildStatus}: it carries
49
+ * the same `child_id`/`pid`/`state`/`restart_count` fields (so it projects onto
50
+ * a {@link ChildStatus} without a lossy cast) plus an optional human-readable
51
+ * `detail` for diagnostics that never crosses the IPC wire.
52
+ *
53
+ * The `child_id` is the subsystem's logical name, matching the id the
54
+ * supervisor tracks the corresponding child under.
55
+ */
56
+ export const SubsystemHealthSchema = ChildStatusSchema.extend({
57
+ /**
58
+ * Optional human-readable detail (e.g. last error message, queue depth).
59
+ * Diagnostic only — not part of the supervisor wire contract.
60
+ */
61
+ detail: z.string().optional(),
62
+ }).strict();
63
+
64
+ /** A single subsystem's health row (inferred from {@link SubsystemHealthSchema}). */
65
+ export type SubsystemHealth = z.infer<typeof SubsystemHealthSchema>;
66
+
67
+ /**
68
+ * The aggregate health snapshot across every registered subsystem.
69
+ *
70
+ * Projects losslessly onto the supervisor's `monitor` snapshot: each
71
+ * {@link SubsystemHealth} row's supervisor-visible fields ARE a
72
+ * {@link ChildStatus}, so {@link toMonitorChildren} can hand the supervisor a
73
+ * `MonitorResponse.children` array with zero casts.
74
+ */
75
+ export const HealthStatusSchema = z
76
+ .object({
77
+ /** One health row per registered subsystem. */
78
+ subsystems: z.array(SubsystemHealthSchema),
79
+ /**
80
+ * `true` iff every subsystem is in the `running` state. A convenience roll-up
81
+ * derived from {@link HealthStatusSchema.shape.subsystems}; recomputed by
82
+ * {@link summarizeHealth} so producers cannot desync it.
83
+ */
84
+ allHealthy: z.boolean(),
85
+ })
86
+ .strict();
87
+
88
+ /** The aggregate daemon health snapshot (inferred from {@link HealthStatusSchema}). */
89
+ export type HealthStatus = z.infer<typeof HealthStatusSchema>;
90
+
91
+ /**
92
+ * Project a {@link HealthStatus} onto the FROZEN supervisor `monitor` row array
93
+ * (`MonitorResponse['children']`) without a lossy cast.
94
+ *
95
+ * Each {@link SubsystemHealth} already carries the four supervisor-visible
96
+ * fields of a {@link ChildStatus}; this strips the daemon-only `detail` so the
97
+ * result is byte-compatible with what `cleo-supervisor` aggregates.
98
+ *
99
+ * @param health - The aggregate daemon health snapshot.
100
+ * @returns An array of {@link ChildStatus} rows for a `MonitorResponse`.
101
+ */
102
+ export function toMonitorChildren(health: HealthStatus): ChildStatus[] {
103
+ return health.subsystems.map((row): ChildStatus => {
104
+ const { detail: _detail, ...childStatus } = row;
105
+ return childStatus;
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Build the aggregate {@link HealthStatus} from per-subsystem rows, computing
111
+ * the `allHealthy` roll-up so it can never desync from the row states.
112
+ *
113
+ * @param subsystems - One {@link SubsystemHealth} row per registered subsystem.
114
+ * @returns The aggregate health snapshot.
115
+ */
116
+ export function summarizeHealth(subsystems: SubsystemHealth[]): HealthStatus {
117
+ return {
118
+ subsystems,
119
+ allHealthy: subsystems.every((row) => row.state === 'running'),
120
+ };
121
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `@cleocode/contracts/daemon` — daemon lifecycle + subsystem type contracts.
3
+ *
4
+ * The shared, IO-free type vocabulary for the `@cleocode/runtime/daemon`
5
+ * submodule (R2, T11253): the {@link Subsystem} descriptor, the typed
6
+ * {@link DaemonLifecycleHooks}, and the {@link HealthStatus} aggregate that
7
+ * composes the FROZEN `supervisor-ipc` v1.0 contract.
8
+ *
9
+ * The implementation (`defineSubsystem`, registry, NDJSON client) lives in
10
+ * `@cleocode/runtime/daemon` and consumes these types — never redeclares them
11
+ * (Contracts Fan-Out gate, T10074).
12
+ *
13
+ * @packageDocumentation
14
+ * @module @cleocode/contracts/daemon
15
+ *
16
+ * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule
17
+ * @task T11366 — daemon lifecycle + subsystem contracts
18
+ * @saga T11243 SG-RUNTIME-UNIFICATION
19
+ */
20
+
21
+ export type { HealthStatus, SubsystemHealth, SubsystemState } from './health.js';
22
+ export {
23
+ HealthStatusSchema,
24
+ SubsystemHealthSchema,
25
+ SubsystemStateSchema,
26
+ summarizeHealth,
27
+ toMonitorChildren,
28
+ } from './health.js';
29
+ export type {
30
+ DaemonLifecycleHooks,
31
+ Subsystem,
32
+ SubsystemDefinition,
33
+ SubsystemLifecyclePhase,
34
+ } from './subsystem.js';
35
+ export { SUBSYSTEM_LIFECYCLE_PHASES } from './subsystem.js';
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Daemon subsystem + lifecycle-hook contracts.
3
+ *
4
+ * A **subsystem** is one supervised unit of long-running work inside a CLEO
5
+ * daemon process — Studio supervision, the GC cron, the web server, the
6
+ * docs-viewer, the runtime poller. Each declares a uniform lifecycle
7
+ * (`start → healthProbe → shutdown`) so the `@cleocode/runtime/daemon`
8
+ * registry can drive them identically and the Rust `cleo-supervisor` can
9
+ * aggregate their health over the FROZEN `supervisor-ipc` v1.0 contract.
10
+ *
11
+ * These are **pure type contracts** (no runtime/IO). The implementation —
12
+ * `defineSubsystem()`, the registry, the NDJSON client — lives in
13
+ * `@cleocode/runtime/daemon` (R2 child T11367) and imports these types rather
14
+ * than redeclaring them (Contracts Fan-Out gate, T10074).
15
+ *
16
+ * @packageDocumentation
17
+ * @module @cleocode/contracts/daemon
18
+ *
19
+ * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule
20
+ * @task T11366 — daemon lifecycle + subsystem contracts
21
+ * @saga T11243 SG-RUNTIME-UNIFICATION
22
+ */
23
+
24
+ import type { SubsystemHealth } from './health.js';
25
+
26
+ /**
27
+ * Lifecycle hooks fired by the subsystem registry around a subsystem's
28
+ * `start`/`shutdown` transitions.
29
+ *
30
+ * All hooks are optional and may be async. They are observational — a hook
31
+ * MUST NOT mutate registry state. `onError` is the single failure sink: a
32
+ * throwing `start` or `shutdown`, or a rejected `healthProbe`, surfaces here.
33
+ *
34
+ * Hooks fire in a defined order relative to the underlying transition:
35
+ * - `onStart` — after the subsystem's `start()` resolves.
36
+ * - `onShutdown` — after the subsystem's `shutdown()` resolves.
37
+ * - `onError` — when any lifecycle step throws/rejects (start, probe, or
38
+ * shutdown), with the offending `error` and the `phase` it occurred in.
39
+ */
40
+ export interface DaemonLifecycleHooks {
41
+ /**
42
+ * Fired after the subsystem has successfully started.
43
+ *
44
+ * @param name - The logical subsystem name.
45
+ */
46
+ onStart?: (name: string) => void | Promise<void>;
47
+ /**
48
+ * Fired after the subsystem has successfully shut down.
49
+ *
50
+ * @param name - The logical subsystem name.
51
+ */
52
+ onShutdown?: (name: string) => void | Promise<void>;
53
+ /**
54
+ * Fired when a lifecycle step throws or rejects. The single failure sink.
55
+ *
56
+ * @param name - The logical subsystem name.
57
+ * @param error - The thrown value, normalized to an `Error`.
58
+ * @param phase - Which lifecycle step failed.
59
+ */
60
+ onError?: (name: string, error: Error, phase: SubsystemLifecyclePhase) => void | Promise<void>;
61
+ }
62
+
63
+ /**
64
+ * The lifecycle phase a subsystem transition is in, used to tag
65
+ * {@link DaemonLifecycleHooks.onError} and registry diagnostics.
66
+ */
67
+ export type SubsystemLifecyclePhase = 'start' | 'healthProbe' | 'shutdown';
68
+
69
+ /**
70
+ * The frozen, ordered set of lifecycle phases. Pinned by the daemon test suite
71
+ * so a phase cannot be silently added or removed.
72
+ */
73
+ export const SUBSYSTEM_LIFECYCLE_PHASES = ['start', 'healthProbe', 'shutdown'] as const;
74
+
75
+ /**
76
+ * A subsystem descriptor — the declarative unit `defineSubsystem()` produces
77
+ * and the registry drives.
78
+ *
79
+ * The three lifecycle methods are intentionally minimal and uniform so that
80
+ * every long-running concern (supervised child process, cron job, HTTP server)
81
+ * can be expressed identically:
82
+ *
83
+ * - `start` — bring the subsystem up. May be async. Idempotent callers
84
+ * should guard re-entry.
85
+ * - `healthProbe` — report current liveness as a {@link SubsystemHealth} row.
86
+ * Maps onto a supervised `ChildStatus` the Rust supervisor aggregates.
87
+ * - `shutdown` — bring the subsystem down gracefully. May be async.
88
+ *
89
+ * @typeParam TContext - Optional opaque context the registry threads through
90
+ * `start`/`shutdown` (e.g. a resolved config or handle). Defaults to `void`.
91
+ */
92
+ export interface Subsystem<TContext = void> {
93
+ /** Logical, stable subsystem name (matches the supervised `child_id`). */
94
+ readonly name: string;
95
+ /**
96
+ * Bring the subsystem up. The resolved value is threaded back into
97
+ * `shutdown` as its context argument.
98
+ *
99
+ * @returns The subsystem context (or `void`).
100
+ */
101
+ start: () => TContext | Promise<TContext>;
102
+ /**
103
+ * Report the subsystem's current health as a single
104
+ * {@link SubsystemHealth} row.
105
+ *
106
+ * @returns The current health row.
107
+ */
108
+ healthProbe: () => SubsystemHealth | Promise<SubsystemHealth>;
109
+ /**
110
+ * Bring the subsystem down gracefully.
111
+ *
112
+ * @param context - The value returned by `start`.
113
+ */
114
+ shutdown: (context: TContext) => void | Promise<void>;
115
+ }
116
+
117
+ /**
118
+ * The descriptor passed to `defineSubsystem()` — the same surface as
119
+ * {@link Subsystem} (a 1:1 mapping; `defineSubsystem` validates + freezes it).
120
+ *
121
+ * Exposed as a distinct name so call-sites can annotate the literal they pass
122
+ * without importing the post-construction {@link Subsystem} type.
123
+ *
124
+ * @typeParam TContext - See {@link Subsystem}.
125
+ */
126
+ export type SubsystemDefinition<TContext = void> = Subsystem<TContext>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Freeze + AC8-alias guard for the `@cleocode/contracts/daemon-ipc` surface
3
+ * (T11369 — R2 gates R4-R7).
4
+ *
5
+ * Proves the AC8-named `daemon-ipc` barrel:
6
+ * 1. resolves and re-exports the FROZEN supervisor-ipc v1.0 schemas + version,
7
+ * 2. is byte-identical to the `supervisor-ipc` surface (one contract, two
8
+ * names — no duplicate drift surface),
9
+ * 3. pins `DAEMON_IPC_PROTOCOL_VERSION === SUPERVISOR_IPC_PROTOCOL_VERSION ===
10
+ * '1.0.0'` and the complete frozen message-kind tuple.
11
+ *
12
+ * @task T11369
13
+ */
14
+
15
+ import { describe, expect, it } from 'vitest';
16
+ import {
17
+ SUPERVISOR_IPC_MESSAGE_KINDS,
18
+ SUPERVISOR_IPC_PROTOCOL_VERSION,
19
+ SupervisorIpcEnvelopeSchema as SupervisorEnvelopeFromRoot,
20
+ } from '../../supervisor-ipc/index.js';
21
+ import {
22
+ DAEMON_IPC_PROTOCOL_VERSION,
23
+ SUPERVISOR_IPC_MESSAGE_KINDS as MESSAGE_KINDS_VIA_DAEMON_IPC,
24
+ SupervisorIpcEnvelopeSchema as SupervisorEnvelopeFromDaemonIpc,
25
+ SUPERVISOR_IPC_PROTOCOL_VERSION as VERSION_VIA_DAEMON_IPC,
26
+ } from '../index.js';
27
+
28
+ describe('@cleocode/contracts/daemon-ipc AC8 alias surface (T11369)', () => {
29
+ it('re-exports the SAME frozen schema binding as supervisor-ipc', () => {
30
+ // Referential identity proves it is a re-export, not a duplicate schema.
31
+ expect(SupervisorEnvelopeFromDaemonIpc).toBe(SupervisorEnvelopeFromRoot);
32
+ });
33
+
34
+ it('pins the frozen protocol version through both names', () => {
35
+ expect(VERSION_VIA_DAEMON_IPC).toBe('1.0.0');
36
+ expect(DAEMON_IPC_PROTOCOL_VERSION).toBe('1.0.0');
37
+ expect(DAEMON_IPC_PROTOCOL_VERSION).toBe(SUPERVISOR_IPC_PROTOCOL_VERSION);
38
+ });
39
+
40
+ it('exposes the identical frozen message-kind set through the daemon-ipc name', () => {
41
+ expect([...MESSAGE_KINDS_VIA_DAEMON_IPC]).toEqual([...SUPERVISOR_IPC_MESSAGE_KINDS]);
42
+ });
43
+
44
+ it('validates a real Rust wire envelope through the daemon-ipc schema', () => {
45
+ const wire = {
46
+ protocol_version: '1.0.0',
47
+ id: 'abc',
48
+ direction: 'request',
49
+ request: { kind: 'health' },
50
+ };
51
+ expect(SupervisorEnvelopeFromDaemonIpc.safeParse(wire).success).toBe(true);
52
+ });
53
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `@cleocode/contracts/daemon-ipc` — the FROZEN v1.0 daemon-IPC surface
3
+ * R4-R7 adapt against (T11253 AC8).
4
+ *
5
+ * ## AC8 reconciliation (T11369)
6
+ *
7
+ * Epic T11253 AC8 specifies the frozen surface is published under the name
8
+ * `@cleocode/contracts/daemon-ipc/`. R1 (T11339) had already shipped the
9
+ * byte-for-byte Rust mirror under `@cleocode/contracts/supervisor-ipc/`. Rather
10
+ * than DUPLICATE the contract (which would create two drift surfaces), this
11
+ * module is a **thin re-export barrel**: `supervisor-ipc` IS the frozen v1.0
12
+ * contract, and `daemon-ipc` is the AC8-named alias the R2 epic promised.
13
+ *
14
+ * Both import sites resolve to the same FROZEN schemas + version constant:
15
+ *
16
+ * ```ts
17
+ * import { SupervisorIpcEnvelopeSchema } from '@cleocode/contracts/daemon-ipc';
18
+ * import { SupervisorIpcEnvelopeSchema } from '@cleocode/contracts/supervisor-ipc';
19
+ * // identical binding — one frozen contract, two names.
20
+ * ```
21
+ *
22
+ * R4-R7 are **pure adapters** against this frozen surface (see the
23
+ * contract-evolution policy doc, slug `daemon-ipc-contract-evolution-policy`).
24
+ * Breaking changes require a NEW versioned directory + a version-negotiation
25
+ * shim + an R2 amendment task — never an in-place edit to the v1.0 schemas.
26
+ *
27
+ * @packageDocumentation
28
+ * @module @cleocode/contracts/daemon-ipc
29
+ *
30
+ * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule
31
+ * @task T11369 — freeze + publish v1.0 daemon-IPC contract surface (AC8 + AC9)
32
+ * @saga T11243 SG-RUNTIME-UNIFICATION
33
+ */
34
+
35
+ export type {
36
+ ChildState,
37
+ ChildStatus,
38
+ EnvPair,
39
+ ErrorResponse,
40
+ HealthRequest,
41
+ HealthResponse,
42
+ LifecycleEventKind,
43
+ LifecycleEventResponse,
44
+ MonitorRequest,
45
+ MonitorResponse,
46
+ RestartedResponse,
47
+ RestartRequest,
48
+ SpawnedResponse,
49
+ SpawnRequest,
50
+ SupervisorIpcEnvelope,
51
+ SupervisorIpcRequest,
52
+ SupervisorIpcRequestEnvelope,
53
+ SupervisorIpcResponse,
54
+ SupervisorIpcResponseEnvelope,
55
+ } from '../supervisor-ipc/index.js';
56
+ export {
57
+ ChildStateSchema,
58
+ ChildStatusSchema,
59
+ EnvPairSchema,
60
+ ErrorResponseSchema,
61
+ HealthRequestSchema,
62
+ HealthResponseSchema,
63
+ LifecycleEventKindSchema,
64
+ LifecycleEventResponseSchema,
65
+ MonitorRequestSchema,
66
+ MonitorResponseSchema,
67
+ RestartedResponseSchema,
68
+ RestartRequestSchema,
69
+ SpawnedResponseSchema,
70
+ SpawnRequestSchema,
71
+ SUPERVISOR_IPC_CHANNEL_BASENAME,
72
+ SUPERVISOR_IPC_MESSAGE_KINDS,
73
+ SUPERVISOR_IPC_PROTOCOL_VERSION,
74
+ SUPERVISOR_IPC_REQUEST_KINDS,
75
+ SUPERVISOR_IPC_RESPONSE_KINDS,
76
+ SupervisorIpcEnvelopeSchema,
77
+ SupervisorIpcRequestEnvelopeSchema,
78
+ SupervisorIpcRequestSchema,
79
+ SupervisorIpcResponseEnvelopeSchema,
80
+ SupervisorIpcResponseSchema,
81
+ } from '../supervisor-ipc/index.js';
82
+
83
+ /**
84
+ * The AC8-canonical name for the frozen daemon-IPC protocol version.
85
+ *
86
+ * Alias of `SUPERVISOR_IPC_PROTOCOL_VERSION` — re-exported so the freeze/drift
87
+ * test (T11369) can pin the value through the `daemon-ipc` surface name AC8
88
+ * specifies. Both constants are the SAME frozen `'1.0.0'`.
89
+ */
90
+ export { SUPERVISOR_IPC_PROTOCOL_VERSION as DAEMON_IPC_PROTOCOL_VERSION } from '../supervisor-ipc/version.js';
@@ -22,7 +22,7 @@
22
22
  "role": "brain",
23
23
  "tier": "project",
24
24
  "filePathTemplate": "<projectRoot>/.cleo/brain.db",
25
- "drizzleSchemaPath": "packages/core/src/store/memory-schema.ts",
25
+ "drizzleSchemaPath": "packages/core/src/store/schema/memory-schema.ts",
26
26
  "migrationsDir": "packages/core/migrations/drizzle-brain/",
27
27
  "ownerPackage": "@cleocode/brain",
28
28
  "openedVia": "openCleoDb('brain', cwd)",
@@ -35,7 +35,7 @@
35
35
  "role": "conduit",
36
36
  "tier": "project",
37
37
  "filePathTemplate": "<projectRoot>/.cleo/conduit.db",
38
- "drizzleSchemaPath": "packages/core/src/store/conduit-schema.ts",
38
+ "drizzleSchemaPath": "packages/core/src/store/schema/conduit-schema.ts",
39
39
  "migrationsDir": "packages/core/migrations/drizzle-conduit/",
40
40
  "ownerPackage": "@cleocode/core",
41
41
  "openedVia": "openCleoDb('conduit', cwd)",
@@ -74,7 +74,7 @@
74
74
  "role": "nexus",
75
75
  "tier": "global",
76
76
  "filePathTemplate": "$XDG_DATA_HOME/cleo/nexus.db",
77
- "drizzleSchemaPath": "packages/core/src/store/nexus-schema.ts",
77
+ "drizzleSchemaPath": "packages/core/src/store/schema/nexus-schema.ts",
78
78
  "migrationsDir": "packages/core/migrations/drizzle-nexus/",
79
79
  "ownerPackage": "@cleocode/core",
80
80
  "openedVia": "openCleoDb('nexus')",
@@ -87,7 +87,7 @@
87
87
  "role": "signaldock-project",
88
88
  "tier": "project",
89
89
  "filePathTemplate": "<projectRoot>/.cleo/signaldock.db",
90
- "drizzleSchemaPath": "packages/core/src/store/signaldock-schema.ts",
90
+ "drizzleSchemaPath": "packages/core/src/store/schema/signaldock-schema.ts",
91
91
  "migrationsDir": "packages/core/migrations/drizzle-signaldock/",
92
92
  "ownerPackage": "@cleocode/core",
93
93
  "openedVia": "HISTORICAL — project-tier signaldock.db was migrated into conduit.db post-T310/ADR-037. No live opener; charter row retained for migration provenance and legacy backup detection.",
@@ -100,7 +100,7 @@
100
100
  "role": "signaldock-global",
101
101
  "tier": "global",
102
102
  "filePathTemplate": "$XDG_DATA_HOME/cleo/signaldock.db",
103
- "drizzleSchemaPath": "packages/core/src/store/signaldock-schema.ts",
103
+ "drizzleSchemaPath": "packages/core/src/store/schema/signaldock-schema.ts",
104
104
  "migrationsDir": "packages/core/migrations/drizzle-signaldock/",
105
105
  "ownerPackage": "@cleocode/core",
106
106
  "openedVia": "openCleoDb('signaldock')",
@@ -113,7 +113,7 @@
113
113
  "role": "telemetry",
114
114
  "tier": "global",
115
115
  "filePathTemplate": "$XDG_DATA_HOME/cleo/telemetry.db",
116
- "drizzleSchemaPath": "packages/core/src/telemetry/schema.ts",
116
+ "drizzleSchemaPath": "packages/core/src/store/schema/telemetry-schema.ts",
117
117
  "migrationsDir": "packages/core/migrations/drizzle-telemetry/",
118
118
  "ownerPackage": "@cleocode/core",
119
119
  "openedVia": "@cleocode/core telemetry/sqlite.ts — lazy open on first event when opted-in; not yet registered as openCleoDb role",
@@ -126,7 +126,7 @@
126
126
  "role": "skills",
127
127
  "tier": "global",
128
128
  "filePathTemplate": "$XDG_DATA_HOME/cleo/skills.db",
129
- "drizzleSchemaPath": "packages/core/src/store/skills-schema.ts",
129
+ "drizzleSchemaPath": "packages/core/src/store/schema/skills-schema.ts",
130
130
  "migrationsDir": "packages/core/migrations/drizzle-skills/",
131
131
  "ownerPackage": "@cleocode/core",
132
132
  "openedVia": "openCleoDb('skills')",
@@ -139,7 +139,7 @@
139
139
  "role": "global-brain",
140
140
  "tier": "global",
141
141
  "filePathTemplate": "$XDG_DATA_HOME/cleo/brain.db",
142
- "drizzleSchemaPath": "packages/core/src/store/memory-schema.ts",
142
+ "drizzleSchemaPath": "packages/core/src/store/schema/memory-schema.ts",
143
143
  "migrationsDir": "packages/core/migrations/drizzle-brain/",
144
144
  "ownerPackage": "@cleocode/brain",
145
145
  "openedVia": "UNREGISTERED — file exists on disk (audit §1.2 row 3) but no live opener. Provenance unclear; likely orphan from a getCleoHome()-vs-project-resolution path bug. Tracked for cleanup decision under T10282/T10307.",