@lumenflow/cli 5.5.0 → 5.7.12

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 (213) hide show
  1. package/README.md +42 -40
  2. package/dist/db-journal-recover.js +400 -0
  3. package/dist/db-journal-recover.js.map +1 -0
  4. package/dist/docs-sync.js +8 -3
  5. package/dist/docs-sync.js.map +1 -1
  6. package/dist/gate-defaults.js +37 -0
  7. package/dist/gate-defaults.js.map +1 -1
  8. package/dist/gates/monolithic-file-contention-guard.js +167 -0
  9. package/dist/gates/monolithic-file-contention-guard.js.map +1 -0
  10. package/dist/gates/prod-migration-drift.js +207 -0
  11. package/dist/gates/prod-migration-drift.js.map +1 -0
  12. package/dist/gates/test-over-deletion-guard.js +255 -0
  13. package/dist/gates/test-over-deletion-guard.js.map +1 -0
  14. package/dist/gates-runners.js +44 -3
  15. package/dist/gates-runners.js.map +1 -1
  16. package/dist/gates.js +3 -2
  17. package/dist/gates.js.map +1 -1
  18. package/dist/lumenflow-setup.js +144 -0
  19. package/dist/lumenflow-setup.js.map +1 -0
  20. package/dist/lumenflow-upgrade.js +2 -1
  21. package/dist/lumenflow-upgrade.js.map +1 -1
  22. package/dist/mem-create.js +10 -1
  23. package/dist/mem-create.js.map +1 -1
  24. package/dist/mem-signal.js +21 -4
  25. package/dist/mem-signal.js.map +1 -1
  26. package/dist/orchestrate-initiative.js +28 -3
  27. package/dist/orchestrate-initiative.js.map +1 -1
  28. package/dist/public-manifest.js +17 -0
  29. package/dist/public-manifest.js.map +1 -1
  30. package/dist/release.js +53 -18
  31. package/dist/release.js.map +1 -1
  32. package/dist/wu-done-gates.js +13 -9
  33. package/dist/wu-done-gates.js.map +1 -1
  34. package/dist/wu-edit-operations.js +74 -0
  35. package/dist/wu-edit-operations.js.map +1 -1
  36. package/dist/wu-edit-validators.js +58 -0
  37. package/dist/wu-edit-validators.js.map +1 -1
  38. package/dist/wu-edit.js +106 -4
  39. package/dist/wu-edit.js.map +1 -1
  40. package/dist/wu-prep.js +41 -7
  41. package/dist/wu-prep.js.map +1 -1
  42. package/dist/wu-recover.js +6 -0
  43. package/dist/wu-recover.js.map +1 -1
  44. package/dist/wu-release.js +120 -2
  45. package/dist/wu-release.js.map +1 -1
  46. package/dist/wu-sizing-validation.js +47 -17
  47. package/dist/wu-sizing-validation.js.map +1 -1
  48. package/dist/wu-status.js +33 -0
  49. package/dist/wu-status.js.map +1 -1
  50. package/package.json +13 -11
  51. package/packs/agent-runtime/package.json +1 -1
  52. package/packs/sidekick/package.json +1 -1
  53. package/packs/software-delivery/package.json +1 -1
  54. package/templates/core/AGENTS.md.template +67 -3
  55. package/templates/core/LUMENFLOW.md.template +197 -47
  56. package/packs/agent-runtime/agent-heartbeat.ts +0 -163
  57. package/packs/agent-runtime/auto-session-integration.ts +0 -888
  58. package/packs/agent-runtime/capability-factory.ts +0 -104
  59. package/packs/agent-runtime/constants.ts +0 -21
  60. package/packs/agent-runtime/delegation-registry-schema.ts +0 -220
  61. package/packs/agent-runtime/delegation-registry-store.ts +0 -269
  62. package/packs/agent-runtime/delegation-tree.ts +0 -328
  63. package/packs/agent-runtime/index.ts +0 -20
  64. package/packs/agent-runtime/manifest.ts +0 -348
  65. package/packs/agent-runtime/memory-coordination-contract.ts +0 -86
  66. package/packs/agent-runtime/orchestration.ts +0 -2027
  67. package/packs/agent-runtime/pack-registration.ts +0 -110
  68. package/packs/agent-runtime/policy-factory.ts +0 -165
  69. package/packs/agent-runtime/remote-controls/index.ts +0 -7
  70. package/packs/agent-runtime/remote-controls/operations.ts +0 -405
  71. package/packs/agent-runtime/remote-controls/port.ts +0 -48
  72. package/packs/agent-runtime/remote-controls/state-store.ts +0 -258
  73. package/packs/agent-runtime/remote-controls/types.ts +0 -105
  74. package/packs/agent-runtime/session-schema.ts +0 -467
  75. package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +0 -793
  76. package/packs/agent-runtime/tool-impl/index.ts +0 -6
  77. package/packs/agent-runtime/tool-impl/provider-adapters.ts +0 -1245
  78. package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +0 -256
  79. package/packs/agent-runtime/tool-impl/remote-controls.ts +0 -273
  80. package/packs/agent-runtime/tools/index.ts +0 -4
  81. package/packs/agent-runtime/tools/types.ts +0 -47
  82. package/packs/agent-runtime/turn-lifecycle-events.ts +0 -590
  83. package/packs/agent-runtime/types.ts +0 -128
  84. package/packs/agent-runtime/vitest.config.ts +0 -11
  85. package/packs/sidekick/channel-ingress.ts +0 -137
  86. package/packs/sidekick/constants.ts +0 -10
  87. package/packs/sidekick/index.ts +0 -8
  88. package/packs/sidekick/manifest-schema.ts +0 -49
  89. package/packs/sidekick/manifest.ts +0 -512
  90. package/packs/sidekick/pack-registration.ts +0 -110
  91. package/packs/sidekick/policy-factory.ts +0 -38
  92. package/packs/sidekick/sidekick-events.ts +0 -694
  93. package/packs/sidekick/src/adapters/cloud-queue.ts +0 -101
  94. package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +0 -386
  95. package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +0 -228
  96. package/packs/sidekick/src/domain/channel.types.ts +0 -64
  97. package/packs/sidekick/src/ports/channel-bridge.port.ts +0 -92
  98. package/packs/sidekick/src/routines/commit.ts +0 -74
  99. package/packs/sidekick/tool-impl/channel-tools.ts +0 -577
  100. package/packs/sidekick/tool-impl/channel-transports.ts +0 -75
  101. package/packs/sidekick/tool-impl/index.ts +0 -29
  102. package/packs/sidekick/tool-impl/memory-tools.ts +0 -290
  103. package/packs/sidekick/tool-impl/routine-commit.ts +0 -102
  104. package/packs/sidekick/tool-impl/routine-tools.ts +0 -440
  105. package/packs/sidekick/tool-impl/runtime-context.ts +0 -28
  106. package/packs/sidekick/tool-impl/shared.ts +0 -125
  107. package/packs/sidekick/tool-impl/storage.ts +0 -325
  108. package/packs/sidekick/tool-impl/system-tools.ts +0 -160
  109. package/packs/sidekick/tool-impl/task-tools.ts +0 -506
  110. package/packs/sidekick/tools/channel-tools.ts +0 -53
  111. package/packs/sidekick/tools/index.ts +0 -9
  112. package/packs/sidekick/tools/memory-tools.ts +0 -53
  113. package/packs/sidekick/tools/routine-tools.ts +0 -53
  114. package/packs/sidekick/tools/system-tools.ts +0 -47
  115. package/packs/sidekick/tools/task-tools.ts +0 -61
  116. package/packs/sidekick/tools/types.ts +0 -57
  117. package/packs/sidekick/vitest.config.ts +0 -11
  118. package/packs/software-delivery/constants.ts +0 -10
  119. package/packs/software-delivery/extensions.ts +0 -140
  120. package/packs/software-delivery/gate-policies.ts +0 -134
  121. package/packs/software-delivery/index.ts +0 -8
  122. package/packs/software-delivery/manifest-schema.ts +0 -268
  123. package/packs/software-delivery/manifest.ts +0 -657
  124. package/packs/software-delivery/pack-registration.ts +0 -113
  125. package/packs/software-delivery/src/commands/index.ts +0 -5
  126. package/packs/software-delivery/src/config/delivery-review-contract.ts +0 -256
  127. package/packs/software-delivery/src/config/env-accessors.ts +0 -66
  128. package/packs/software-delivery/src/config/index.ts +0 -8
  129. package/packs/software-delivery/src/config/normalize-config-keys.ts +0 -9
  130. package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +0 -460
  131. package/packs/software-delivery/src/config/workspace-reader.ts +0 -375
  132. package/packs/software-delivery/src/constants/backlog-patterns.ts +0 -31
  133. package/packs/software-delivery/src/constants/client-ids.ts +0 -19
  134. package/packs/software-delivery/src/constants/config-contract.ts +0 -7
  135. package/packs/software-delivery/src/constants/docs-layout-presets.ts +0 -50
  136. package/packs/software-delivery/src/constants/duration-constants.ts +0 -20
  137. package/packs/software-delivery/src/constants/gate-constants.ts +0 -32
  138. package/packs/software-delivery/src/constants/index.ts +0 -29
  139. package/packs/software-delivery/src/constants/lock-constants.ts +0 -35
  140. package/packs/software-delivery/src/constants/object-guards.ts +0 -12
  141. package/packs/software-delivery/src/constants/section-headings.ts +0 -107
  142. package/packs/software-delivery/src/constants/wu-cli-constants.ts +0 -500
  143. package/packs/software-delivery/src/constants/wu-domain-constants.ts +0 -466
  144. package/packs/software-delivery/src/constants/wu-git-constants.ts +0 -7
  145. package/packs/software-delivery/src/constants/wu-id-format.ts +0 -327
  146. package/packs/software-delivery/src/constants/wu-paths-constants.ts +0 -384
  147. package/packs/software-delivery/src/constants/wu-statuses.ts +0 -287
  148. package/packs/software-delivery/src/constants/wu-type-helpers.ts +0 -67
  149. package/packs/software-delivery/src/constants/wu-ui-constants.ts +0 -267
  150. package/packs/software-delivery/src/constants/wu-validation-constants.ts +0 -73
  151. package/packs/software-delivery/src/domain/index.ts +0 -5
  152. package/packs/software-delivery/src/domain/orchestration.constants.ts +0 -166
  153. package/packs/software-delivery/src/domain/orchestration.schemas.ts +0 -238
  154. package/packs/software-delivery/src/domain/orchestration.types.ts +0 -176
  155. package/packs/software-delivery/src/methodology/incremental-test.ts +0 -122
  156. package/packs/software-delivery/src/methodology/index.ts +0 -6
  157. package/packs/software-delivery/src/methodology/manual-test-validator.ts +0 -292
  158. package/packs/software-delivery/src/policy/coverage-gate.ts +0 -270
  159. package/packs/software-delivery/src/policy/gates-agent-mode.ts +0 -223
  160. package/packs/software-delivery/src/policy/gates-config-internal.ts +0 -121
  161. package/packs/software-delivery/src/policy/gates-config.ts +0 -300
  162. package/packs/software-delivery/src/policy/gates-coverage.ts +0 -356
  163. package/packs/software-delivery/src/policy/gates-presets.ts +0 -134
  164. package/packs/software-delivery/src/policy/gates-schemas.ts +0 -173
  165. package/packs/software-delivery/src/policy/index.ts +0 -22
  166. package/packs/software-delivery/src/policy/package-manager-resolver.ts +0 -319
  167. package/packs/software-delivery/src/policy/resolve-policy.ts +0 -601
  168. package/packs/software-delivery/src/ports/config.ports.ts +0 -90
  169. package/packs/software-delivery/src/ports/dashboard-renderer.port.ts +0 -125
  170. package/packs/software-delivery/src/ports/index.ts +0 -10
  171. package/packs/software-delivery/src/ports/sync-validator.ports.ts +0 -59
  172. package/packs/software-delivery/src/ports/wu-helpers.ports.ts +0 -168
  173. package/packs/software-delivery/src/ports/wu-state.ports.ts +0 -241
  174. package/packs/software-delivery/src/primitives/index.ts +0 -5
  175. package/packs/software-delivery/src/runtime/index.ts +0 -6
  176. package/packs/software-delivery/src/runtime/work-classifier.ts +0 -561
  177. package/packs/software-delivery/src/sandbox/index.ts +0 -10
  178. package/packs/software-delivery/src/sandbox/sandbox-allowlist.ts +0 -118
  179. package/packs/software-delivery/src/sandbox/sandbox-backend-linux.ts +0 -88
  180. package/packs/software-delivery/src/sandbox/sandbox-backend-macos.ts +0 -154
  181. package/packs/software-delivery/src/sandbox/sandbox-backend-windows.ts +0 -47
  182. package/packs/software-delivery/src/sandbox/sandbox-profile.ts +0 -153
  183. package/packs/software-delivery/src/schemas/index.ts +0 -5
  184. package/packs/software-delivery/src/state/date-utils.ts +0 -158
  185. package/packs/software-delivery/src/state/index.ts +0 -15
  186. package/packs/software-delivery/src/state/state-machine.ts +0 -119
  187. package/packs/software-delivery/src/state/wu-doc-types.ts +0 -51
  188. package/packs/software-delivery/src/state/wu-paths.ts +0 -381
  189. package/packs/software-delivery/src/state/wu-schema.ts +0 -1139
  190. package/packs/software-delivery/src/state/wu-state-schema.ts +0 -255
  191. package/packs/software-delivery/src/state/wu-yaml.ts +0 -338
  192. package/packs/software-delivery/tool-impl/agent-tools.ts +0 -263
  193. package/packs/software-delivery/tool-impl/delegation-tools.ts +0 -66
  194. package/packs/software-delivery/tool-impl/flow-metrics-tools.ts +0 -219
  195. package/packs/software-delivery/tool-impl/git-runner.ts +0 -113
  196. package/packs/software-delivery/tool-impl/git-tools.ts +0 -316
  197. package/packs/software-delivery/tool-impl/index.ts +0 -15
  198. package/packs/software-delivery/tool-impl/initiative-orchestration-tools.ts +0 -720
  199. package/packs/software-delivery/tool-impl/lane-lock.ts +0 -246
  200. package/packs/software-delivery/tool-impl/memory-tools.ts +0 -470
  201. package/packs/software-delivery/tool-impl/pending-runtime-tools.ts +0 -21
  202. package/packs/software-delivery/tool-impl/runtime-cli-adapter.ts +0 -329
  203. package/packs/software-delivery/tool-impl/runtime-native-tools.ts +0 -687
  204. package/packs/software-delivery/tool-impl/worker-loader.ts +0 -52
  205. package/packs/software-delivery/tool-impl/worktree-tools.ts +0 -46
  206. package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +0 -807
  207. package/packs/software-delivery/tools/delegation-tools.ts +0 -23
  208. package/packs/software-delivery/tools/git-tools.ts +0 -55
  209. package/packs/software-delivery/tools/index.ts +0 -8
  210. package/packs/software-delivery/tools/lane-lock-tool.ts +0 -37
  211. package/packs/software-delivery/tools/types.ts +0 -71
  212. package/packs/software-delivery/tools/worktree-tools.ts +0 -49
  213. package/packs/software-delivery/vitest.config.ts +0 -11
@@ -1,888 +0,0 @@
1
- // Copyright (c) 2026 Hellmai Ltd
2
- // SPDX-License-Identifier: LicenseRef-LumenFlow-Proprietary
3
-
4
- /**
5
- * Auto-Session Integration for wu:claim and wu:done lifecycle (WU-1438, WU-1466)
6
- *
7
- * Provides wrapper functions around agent-session.ts that:
8
- * 1. Auto-start sessions on wu:claim with silent no-op if already active
9
- * 2. Auto-end sessions on wu:done with silent no-op if not active
10
- * 3. Store session_id in WU YAML for tracking
11
- * 4. Create memory layer session nodes for context restoration (WU-1466)
12
- *
13
- * Design principles:
14
- * - Composition over modification (wraps existing agent-session.ts)
15
- * - Silent failures for idempotent operations (no throw on duplicate start/end)
16
- * - Configurable session directory for testing
17
- */
18
- import { randomUUID } from 'crypto';
19
- import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync, readdirSync } from 'fs';
20
- import { join } from 'path';
21
- import { parse as parseYAML } from 'yaml';
22
- import {
23
- HeartbeatManager,
24
- type AgentHeartbeatInput,
25
- type AgentHeartbeatResult,
26
- type HeartbeatHealth,
27
- } from './agent-heartbeat.js';
28
- import {
29
- acquireDisplayName,
30
- releaseDisplayName,
31
- SESSION_SCHEMA_VERSION_V2,
32
- withV2RoleDefaults,
33
- type SessionSchemaVersion,
34
- } from './session-schema.js';
35
-
36
- const SESSION_FILENAME = 'current.json';
37
- const SESSION_DIR = '.lumenflow/sessions';
38
- const WORKSPACE_CONFIG_FILE = 'workspace.yaml';
39
- const CONTROL_PLANE_REGISTER_PATH = '/api/v1/sessions/register';
40
- const CONTROL_PLANE_DEREGISTER_PATH = '/api/v1/sessions/deregister';
41
- const CONTROL_PLANE_HEARTBEAT_PATH = '/api/v1/heartbeat';
42
-
43
- // Default context tier for auto-started sessions
44
- const DEFAULT_TIER: 1 | 2 | 3 = 2;
45
-
46
- // Agent type for auto-started sessions
47
- const DEFAULT_AGENT_TYPE = 'claude-code';
48
- const DEFAULT_AGENT_VERSION = 'unknown';
49
- const DEFAULT_HOST_ID = 'unknown';
50
- const DEFAULT_AGENT_CAPABILITIES = ['session_lifecycle', 'heartbeat'] as const;
51
- const AGENT_CAPABILITIES_ENV = 'LUMENFLOW_AGENT_CAPABILITIES';
52
- const AGENT_VERSION_ENV = 'LUMENFLOW_AGENT_VERSION';
53
-
54
- type SessionMetadataValue = string | number | boolean | string[];
55
-
56
- /**
57
- * Session data stored in current.json
58
- *
59
- * WU-2754 (ADR-014 extension 2): `schemaVersion`, `lifecycle_role`,
60
- * `specialty_profile`, and `delegation_id` are optional additive fields.
61
- * v1 legacy records (no `schemaVersion`) continue to load without them.
62
- */
63
- interface SessionFileData {
64
- session_id: string;
65
- wu_id: string;
66
- started: string;
67
- last_heartbeat?: string;
68
- completed?: string;
69
- lane?: string;
70
- agent_type: string;
71
- client_type?: string;
72
- capabilities?: string[];
73
- agent_version?: string;
74
- host_id?: string;
75
- context_tier: number;
76
- incidents_logged: number;
77
- incidents_major: number;
78
- auto_started?: boolean;
79
- /** ADR-014 extension 2 — schema version stamp for v2 records (WU-2754). */
80
- schemaVersion?: SessionSchemaVersion;
81
- /** ADR-014 extension 2 — what phase of delivery this session owns (WU-2754). */
82
- lifecycle_role?: string;
83
- /** ADR-014 extension 2 — which skill bundle the agent identifies as carrying (WU-2754). */
84
- specialty_profile?: string;
85
- display_name?: string;
86
- /** Stable explicit A2A reader identity. Format hint: <owner>:<role>:<purpose>. */
87
- agent_identity?: string;
88
- /** Reserved cloud workspace/tenant scope slot; unused by local runtime. */
89
- tenant_id?: string;
90
- /** ADR-014 extension 2 — parent delegation registry reference (WU-2754). */
91
- delegation_id?: string;
92
- }
93
-
94
- interface RegisterSessionInput {
95
- workspace_id: string;
96
- session_id: string;
97
- agent_id: string;
98
- started_at: string;
99
- lane?: string;
100
- wu_id?: string;
101
- client_type?: string;
102
- capabilities?: string[];
103
- agent_version?: string;
104
- host_id?: string;
105
- metadata?: Record<string, SessionMetadataValue>;
106
- }
107
-
108
- interface DeregisterSessionInput {
109
- workspace_id: string;
110
- session_id: string;
111
- ended_at?: string;
112
- reason?: string;
113
- }
114
-
115
- interface ControlPlaneSessionSyncPort {
116
- registerSession(input: RegisterSessionInput): Promise<unknown>;
117
- deregisterSession(input: DeregisterSessionInput): Promise<unknown>;
118
- heartbeat?(input: AgentHeartbeatInput): Promise<AgentHeartbeatResult>;
119
- }
120
-
121
- interface ResolvedControlPlaneSyncConfig {
122
- workspaceId: string;
123
- endpoint: string;
124
- token: string;
125
- }
126
-
127
- interface WorkspaceControlPlaneDocument {
128
- id?: unknown;
129
- control_plane?: {
130
- endpoint?: unknown;
131
- auth?: {
132
- token_env?: unknown;
133
- };
134
- };
135
- }
136
-
137
- function parseCapabilities(rawValue: string | null): string[] {
138
- if (!rawValue) {
139
- return [...DEFAULT_AGENT_CAPABILITIES];
140
- }
141
-
142
- const parsed = rawValue
143
- .split(',')
144
- .map((entry) => entry.trim())
145
- .filter((entry) => entry.length > 0);
146
- return parsed.length > 0 ? parsed : [...DEFAULT_AGENT_CAPABILITIES];
147
- }
148
-
149
- function asNonEmptyString(value: unknown): string | null {
150
- return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
151
- }
152
-
153
- function normalizeEndpoint(endpoint: string): string {
154
- return endpoint.endsWith('/') ? endpoint.slice(0, endpoint.length - 1) : endpoint;
155
- }
156
-
157
- function resolveControlPlaneSessionSyncConfig(
158
- workspaceRoot: string,
159
- environment: NodeJS.ProcessEnv,
160
- ): ResolvedControlPlaneSyncConfig | null {
161
- const workspacePath = join(workspaceRoot, WORKSPACE_CONFIG_FILE);
162
- if (!existsSync(workspacePath)) {
163
- return null;
164
- }
165
-
166
- let parsedWorkspace: WorkspaceControlPlaneDocument | null;
167
- try {
168
- parsedWorkspace = parseYAML(
169
- readFileSync(workspacePath, { encoding: 'utf-8' }),
170
- ) as WorkspaceControlPlaneDocument | null;
171
- } catch {
172
- return null;
173
- }
174
-
175
- const workspaceId = asNonEmptyString(parsedWorkspace?.id);
176
- const endpoint = asNonEmptyString(parsedWorkspace?.control_plane?.endpoint);
177
- const tokenEnv = asNonEmptyString(parsedWorkspace?.control_plane?.auth?.token_env);
178
- if (!workspaceId || !endpoint || !tokenEnv) {
179
- return null;
180
- }
181
-
182
- const token = asNonEmptyString(environment[tokenEnv]);
183
- if (!token) {
184
- return null;
185
- }
186
-
187
- try {
188
- // Validate endpoint before creating outbound requests.
189
- void new URL(endpoint);
190
- } catch {
191
- return null;
192
- }
193
-
194
- return {
195
- workspaceId,
196
- endpoint: normalizeEndpoint(endpoint),
197
- token,
198
- };
199
- }
200
-
201
- function resolveStandardSessionMetadata(input: {
202
- agentType: string;
203
- environment: NodeJS.ProcessEnv;
204
- }): {
205
- client_type: string;
206
- capabilities: string[];
207
- agent_version: string;
208
- host_id: string;
209
- metadata: Record<string, SessionMetadataValue>;
210
- } {
211
- const clientType = input.agentType;
212
- const capabilities = parseCapabilities(
213
- asNonEmptyString(input.environment[AGENT_CAPABILITIES_ENV]),
214
- );
215
- const agentVersion =
216
- asNonEmptyString(input.environment[AGENT_VERSION_ENV]) ?? DEFAULT_AGENT_VERSION;
217
- const hostId =
218
- asNonEmptyString(input.environment.HOSTNAME) ??
219
- asNonEmptyString(input.environment.COMPUTERNAME) ??
220
- DEFAULT_HOST_ID;
221
-
222
- return {
223
- client_type: clientType,
224
- capabilities,
225
- agent_version: agentVersion,
226
- host_id: hostId,
227
- metadata: {
228
- client_type: clientType,
229
- capabilities,
230
- agent_version: agentVersion,
231
- host_id: hostId,
232
- },
233
- };
234
- }
235
-
236
- async function postControlPlaneSessionHook(
237
- endpoint: string,
238
- token: string,
239
- path: string,
240
- payload: object,
241
- fetchFn: typeof fetch,
242
- ): Promise<void> {
243
- const response = await fetchFn(`${endpoint}${path}`, {
244
- method: 'POST',
245
- headers: {
246
- authorization: `Bearer ${token}`,
247
- 'content-type': 'application/json',
248
- },
249
- body: JSON.stringify(payload),
250
- });
251
-
252
- if (!response.ok) {
253
- throw new Error(`HTTP ${response.status}`);
254
- }
255
- }
256
-
257
- function asFiniteNumber(value: unknown): number | undefined {
258
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
259
- }
260
-
261
- function normalizeSessionFileData(session: SessionFileData): SessionFileData {
262
- return {
263
- ...session,
264
- client_type: session.client_type ?? session.agent_type,
265
- capabilities: Array.isArray(session.capabilities)
266
- ? session.capabilities.filter((entry): entry is string => typeof entry === 'string')
267
- : [...DEFAULT_AGENT_CAPABILITIES],
268
- agent_version: session.agent_version ?? DEFAULT_AGENT_VERSION,
269
- host_id: session.host_id ?? DEFAULT_HOST_ID,
270
- };
271
- }
272
-
273
- function normalizeHeartbeatResult(
274
- raw: AgentHeartbeatResult | Record<string, unknown>,
275
- ): AgentHeartbeatResult {
276
- return {
277
- status: 'ok',
278
- server_time:
279
- typeof raw.server_time === 'string' && raw.server_time.length > 0
280
- ? raw.server_time
281
- : new Date().toISOString(),
282
- ...(asFiniteNumber(raw.next_heartbeat_ms) !== undefined
283
- ? { next_heartbeat_ms: asFiniteNumber(raw.next_heartbeat_ms) }
284
- : {}),
285
- ...(asFiniteNumber(raw.budget_remaining_usd) !== undefined
286
- ? { budget_remaining_usd: asFiniteNumber(raw.budget_remaining_usd) }
287
- : {}),
288
- ...(asFiniteNumber(raw.coalesced_signals) !== undefined
289
- ? { coalesced_signals: asFiniteNumber(raw.coalesced_signals) }
290
- : {}),
291
- ...(typeof raw.assignment === 'object' && raw.assignment !== null
292
- ? { assignment: raw.assignment as AgentHeartbeatResult['assignment'] }
293
- : {}),
294
- };
295
- }
296
-
297
- async function postControlPlaneHeartbeat(
298
- endpoint: string,
299
- token: string,
300
- payload: AgentHeartbeatInput,
301
- fetchFn: typeof fetch,
302
- ): Promise<AgentHeartbeatResult> {
303
- const response = await fetchFn(`${endpoint}${CONTROL_PLANE_HEARTBEAT_PATH}`, {
304
- method: 'POST',
305
- headers: {
306
- authorization: `Bearer ${token}`,
307
- 'content-type': 'application/json',
308
- },
309
- body: JSON.stringify(payload),
310
- });
311
-
312
- if (!response.ok) {
313
- throw new Error(`HTTP ${response.status}`);
314
- }
315
-
316
- const raw = (await response.json().catch(() => ({}))) as AgentHeartbeatResult;
317
- return normalizeHeartbeatResult(raw);
318
- }
319
-
320
- /**
321
- * Get the session file path for a given session directory
322
- * @param sessionDir - Session directory path
323
- * @returns Full path to current.json
324
- */
325
- function getSessionFilePath(sessionDir: string): string {
326
- return join(sessionDir, SESSION_FILENAME);
327
- }
328
-
329
- function getCanonicalSessionFilePath(sessionDir: string, wuId: string): string {
330
- return join(sessionDir, `${wuId}.json`);
331
- }
332
-
333
- function ensureSessionDirectory(sessionDir: string): void {
334
- if (!existsSync(sessionDir)) {
335
- mkdirSync(sessionDir, { recursive: true });
336
- }
337
- }
338
-
339
- function readSessionFile(filePath: string): SessionFileData | null {
340
- if (!existsSync(filePath)) {
341
- return null;
342
- }
343
-
344
- return normalizeSessionFileData(
345
- JSON.parse(readFileSync(filePath, { encoding: 'utf-8' })) as SessionFileData,
346
- );
347
- }
348
-
349
- function writeSessionFile(filePath: string, session: SessionFileData): void {
350
- writeFileSync(filePath, JSON.stringify(session, null, 2), { encoding: 'utf-8' });
351
- }
352
-
353
- function getCanonicalSessions(sessionDir: string): SessionFileData[] {
354
- if (!existsSync(sessionDir)) {
355
- return [];
356
- }
357
-
358
- return readdirSync(sessionDir)
359
- .filter((entry) => entry.endsWith('.json') && entry !== SESSION_FILENAME)
360
- .map((entry) => readSessionFile(join(sessionDir, entry)))
361
- .filter((session): session is SessionFileData => session !== null);
362
- }
363
-
364
- function getMostRecentActiveSession(sessionDir: string): SessionFileData | null {
365
- const sessions = getCanonicalSessions(sessionDir);
366
- if (sessions.length === 0) {
367
- return null;
368
- }
369
-
370
- const [latest] = [...sessions].sort((left, right) => {
371
- const leftTime = Date.parse(left.started);
372
- const rightTime = Date.parse(right.started);
373
-
374
- if (Number.isNaN(leftTime) && Number.isNaN(rightTime)) {
375
- return left.wu_id.localeCompare(right.wu_id);
376
- }
377
- if (Number.isNaN(leftTime)) {
378
- return 1;
379
- }
380
- if (Number.isNaN(rightTime)) {
381
- return -1;
382
- }
383
-
384
- return rightTime - leftTime;
385
- });
386
-
387
- return latest ?? null;
388
- }
389
-
390
- function refreshCompatibilityPointer(
391
- sessionDir: string,
392
- preferredSession: SessionFileData | null = null,
393
- ): void {
394
- const compatibilityPath = getSessionFilePath(sessionDir);
395
- const activeSession = preferredSession ?? getMostRecentActiveSession(sessionDir);
396
-
397
- if (!activeSession) {
398
- if (existsSync(compatibilityPath)) {
399
- unlinkSync(compatibilityPath);
400
- }
401
- return;
402
- }
403
-
404
- ensureSessionDirectory(sessionDir);
405
- writeSessionFile(compatibilityPath, activeSession);
406
- }
407
-
408
- function resolveSessionForWU(sessionDir: string, wuId: string): SessionFileData | null {
409
- const canonicalSession = readSessionFile(getCanonicalSessionFilePath(sessionDir, wuId));
410
- if (canonicalSession) {
411
- return canonicalSession;
412
- }
413
-
414
- const compatibilitySession = readSessionFile(getSessionFilePath(sessionDir));
415
- if (compatibilitySession?.wu_id === wuId) {
416
- ensureSessionDirectory(sessionDir);
417
- writeSessionFile(getCanonicalSessionFilePath(sessionDir, wuId), compatibilitySession);
418
- refreshCompatibilityPointer(sessionDir, compatibilitySession);
419
- return compatibilitySession;
420
- }
421
-
422
- return null;
423
- }
424
-
425
- function resolveCurrentCompatibilitySession(sessionDir: string): SessionFileData | null {
426
- const compatibilitySession = readSessionFile(getSessionFilePath(sessionDir));
427
- if (compatibilitySession) {
428
- return compatibilitySession;
429
- }
430
-
431
- const latestSession = getMostRecentActiveSession(sessionDir);
432
- if (latestSession) {
433
- refreshCompatibilityPointer(sessionDir, latestSession);
434
- }
435
- return latestSession;
436
- }
437
-
438
- /**
439
- * Options for starting a session for a WU
440
- *
441
- * WU-2754: `lifecycleRole`, `specialtyProfile`, `delegationId` are optional
442
- * ADR-014 extension 2 axes. When omitted, `startSessionForWU` applies
443
- * kernel-agnostic v2 defaults (executor/general) — the CLI pack layer owns
444
- * the canonical vocabulary (see `delegation-role-resolver.ts`).
445
- */
446
- interface StartSessionOptions {
447
- wuId: string;
448
- tier?: 1 | 2 | 3;
449
- agentType?: string;
450
- lane?: string;
451
- sessionDir?: string;
452
- workspaceRoot?: string;
453
- environment?: NodeJS.ProcessEnv;
454
- controlPlaneSyncPort?: ControlPlaneSessionSyncPort;
455
- fetchFn?: typeof fetch;
456
- baseDir?: string;
457
- /** ADR-014 lifecycle_role for this session (e.g. 'orchestrator'). */
458
- lifecycleRole?: string;
459
- /** ADR-014 specialty_profile for this session (e.g. 'delivery'). */
460
- specialtyProfile?: string;
461
- displayName?: string;
462
- /** Explicit stable A2A reader identity. Format hint: <owner>:<role>:<purpose>. */
463
- agentIdentity?: string;
464
- /** Reserved cloud workspace/tenant scope slot; unused by local runtime. */
465
- tenantId?: string;
466
- /** Parent delegation-registry record reference (dlg-XXXX). */
467
- delegationId?: string;
468
- }
469
-
470
- /**
471
- * Result of starting a session
472
- */
473
- interface StartSessionResult {
474
- sessionId: string;
475
- alreadyActive?: boolean;
476
- memoryNodeId?: string | null;
477
- }
478
-
479
- interface HeartbeatSessionOptions {
480
- wuId?: string;
481
- sessionDir?: string;
482
- workspaceRoot?: string;
483
- environment?: NodeJS.ProcessEnv;
484
- controlPlaneSyncPort?: ControlPlaneSessionSyncPort;
485
- heartbeatManager?: HeartbeatManager;
486
- fetchFn?: typeof fetch;
487
- health?: HeartbeatHealth;
488
- }
489
-
490
- interface HeartbeatSessionResult {
491
- sent: boolean;
492
- reason?: 'no_active_session' | 'control_plane_unavailable';
493
- heartbeat?: AgentHeartbeatResult;
494
- }
495
-
496
- /**
497
- * Start a session for a WU (called by wu:claim)
498
- *
499
- * Unlike startSession in agent-session.ts, this function:
500
- * - Does NOT throw if a session already exists (returns existing session)
501
- * - Uses default tier 2 if not specified
502
- * - Supports custom session directory for testing
503
- * - Creates memory layer session node for context restoration (WU-1466)
504
- *
505
- * @param options - Session options
506
- * @returns Session result
507
- */
508
- export async function startSessionForWU(options: StartSessionOptions): Promise<StartSessionResult> {
509
- const {
510
- wuId,
511
- tier = DEFAULT_TIER,
512
- agentType = DEFAULT_AGENT_TYPE,
513
- lane,
514
- sessionDir,
515
- workspaceRoot = process.cwd(),
516
- environment = process.env,
517
- controlPlaneSyncPort,
518
- fetchFn = fetch,
519
- lifecycleRole,
520
- specialtyProfile,
521
- displayName,
522
- agentIdentity,
523
- tenantId,
524
- delegationId,
525
- } = options;
526
-
527
- const sessDir = sessionDir ?? SESSION_DIR;
528
- const canonicalSessionFile = getCanonicalSessionFilePath(sessDir, wuId);
529
-
530
- const existing = resolveSessionForWU(sessDir, wuId);
531
- if (existing) {
532
- refreshCompatibilityPointer(sessDir, existing);
533
- return {
534
- sessionId: existing.session_id,
535
- alreadyActive: true,
536
- };
537
- }
538
-
539
- // Create session directory if needed
540
- ensureSessionDirectory(sessDir);
541
-
542
- const standardizedMetadata = resolveStandardSessionMetadata({
543
- agentType,
544
- environment,
545
- });
546
-
547
- // Create new session
548
- const sessionId = randomUUID();
549
- // WU-2754 (ADR-014 extension 2): every new session is v2-stamped and
550
- // carries lifecycle_role + specialty_profile (caller-supplied or defaulted).
551
- const roleAxes = withV2RoleDefaults({
552
- lifecycle_role: lifecycleRole,
553
- specialty_profile: specialtyProfile,
554
- display_name: displayName,
555
- agent_identity: agentIdentity,
556
- tenant_id: tenantId,
557
- delegation_id: delegationId,
558
- });
559
- const resolvedDisplayName = roleAxes.display_name ?? acquireDisplayName(workspaceRoot, sessionId);
560
- const session: SessionFileData = {
561
- session_id: sessionId,
562
- wu_id: wuId,
563
- started: new Date().toISOString(),
564
- last_heartbeat: new Date().toISOString(),
565
- lane,
566
- agent_type: agentType,
567
- client_type: standardizedMetadata.client_type,
568
- capabilities: standardizedMetadata.capabilities,
569
- agent_version: standardizedMetadata.agent_version,
570
- host_id: standardizedMetadata.host_id,
571
- context_tier: tier,
572
- incidents_logged: 0,
573
- incidents_major: 0,
574
- auto_started: true, // Mark as auto-started by wu:claim
575
- schemaVersion: SESSION_SCHEMA_VERSION_V2,
576
- lifecycle_role: roleAxes.lifecycle_role,
577
- specialty_profile: roleAxes.specialty_profile,
578
- display_name: resolvedDisplayName,
579
- ...(roleAxes.agent_identity ? { agent_identity: roleAxes.agent_identity } : {}),
580
- ...(roleAxes.tenant_id ? { tenant_id: roleAxes.tenant_id } : {}),
581
- ...(roleAxes.delegation_id ? { delegation_id: roleAxes.delegation_id } : {}),
582
- };
583
-
584
- writeSessionFile(canonicalSessionFile, session);
585
- refreshCompatibilityPointer(sessDir, session);
586
-
587
- // WU-2153: Optional control-plane session registration.
588
- // Fail-open by design: session lifecycle must not be blocked by remote errors.
589
- const controlPlaneConfig = resolveControlPlaneSessionSyncConfig(workspaceRoot, environment);
590
- if (controlPlaneConfig) {
591
- const registerInput: RegisterSessionInput = {
592
- workspace_id: controlPlaneConfig.workspaceId,
593
- session_id: sessionId,
594
- agent_id: agentType,
595
- started_at: session.started,
596
- lane,
597
- wu_id: wuId,
598
- client_type: standardizedMetadata.client_type,
599
- capabilities: standardizedMetadata.capabilities,
600
- agent_version: standardizedMetadata.agent_version,
601
- host_id: standardizedMetadata.host_id,
602
- metadata: standardizedMetadata.metadata,
603
- };
604
-
605
- try {
606
- if (controlPlaneSyncPort) {
607
- await controlPlaneSyncPort.registerSession(registerInput);
608
- } else {
609
- await postControlPlaneSessionHook(
610
- controlPlaneConfig.endpoint,
611
- controlPlaneConfig.token,
612
- CONTROL_PLANE_REGISTER_PATH,
613
- registerInput,
614
- fetchFn,
615
- );
616
- }
617
- } catch {
618
- // Fail-open: remote registration must not block wu:claim.
619
- }
620
- }
621
-
622
- return {
623
- sessionId,
624
- alreadyActive: false,
625
- };
626
- }
627
-
628
- /**
629
- * Send a control-plane heartbeat for the active WU session.
630
- *
631
- * This is fail-open for local workflows: if no active session or no control-plane config
632
- * is present, no heartbeat is sent and a structured reason is returned.
633
- */
634
- export async function heartbeatSessionForWU(
635
- options: HeartbeatSessionOptions = {},
636
- ): Promise<HeartbeatSessionResult> {
637
- const {
638
- wuId,
639
- sessionDir,
640
- workspaceRoot = process.cwd(),
641
- environment = process.env,
642
- controlPlaneSyncPort,
643
- heartbeatManager,
644
- fetchFn = fetch,
645
- health,
646
- } = options;
647
- const sessDir = sessionDir ?? SESSION_DIR;
648
-
649
- const currentSession = getCurrentSessionForWU({ sessionDir, wuId });
650
- if (!currentSession) {
651
- return {
652
- sent: false,
653
- reason: 'no_active_session',
654
- };
655
- }
656
-
657
- const controlPlaneConfig = resolveControlPlaneSessionSyncConfig(workspaceRoot, environment);
658
- if (!controlPlaneConfig) {
659
- return {
660
- sent: false,
661
- reason: 'control_plane_unavailable',
662
- };
663
- }
664
-
665
- const heartbeatPort = controlPlaneSyncPort?.heartbeat
666
- ? {
667
- heartbeat: controlPlaneSyncPort.heartbeat.bind(controlPlaneSyncPort),
668
- }
669
- : {
670
- heartbeat: async (input: AgentHeartbeatInput): Promise<AgentHeartbeatResult> =>
671
- postControlPlaneHeartbeat(
672
- controlPlaneConfig.endpoint,
673
- controlPlaneConfig.token,
674
- input,
675
- fetchFn,
676
- ),
677
- };
678
-
679
- const manager = heartbeatManager ?? new HeartbeatManager(heartbeatPort);
680
- const heartbeat = await manager.heartbeat({
681
- workspace_id: controlPlaneConfig.workspaceId,
682
- session_id: currentSession.session_id,
683
- agent_id: currentSession.agent_type,
684
- wu_id: currentSession.wu_id,
685
- ...(health ? { health } : {}),
686
- });
687
-
688
- currentSession.last_heartbeat = new Date().toISOString();
689
- writeSessionFile(getCanonicalSessionFilePath(sessDir, currentSession.wu_id), currentSession);
690
- refreshCompatibilityPointer(sessDir, currentSession);
691
-
692
- return {
693
- sent: true,
694
- heartbeat,
695
- };
696
- }
697
-
698
- /**
699
- * Options for ending a session
700
- */
701
- interface EndSessionOptions {
702
- wuId?: string;
703
- sessionDir?: string;
704
- workspaceRoot?: string;
705
- environment?: NodeJS.ProcessEnv;
706
- controlPlaneSyncPort?: ControlPlaneSessionSyncPort;
707
- fetchFn?: typeof fetch;
708
- }
709
-
710
- /**
711
- * Session summary
712
- *
713
- * WU-2754: includes optional ADR-014 extension 2 axes (lifecycle_role,
714
- * specialty_profile, delegation_id). Absent on v1 legacy sessions.
715
- */
716
- interface SessionSummary {
717
- wu_id: string;
718
- session_id: string;
719
- started: string;
720
- completed: string;
721
- agent_type: string;
722
- context_tier: number;
723
- incidents_logged: number;
724
- incidents_major: number;
725
- /** ADR-014 extension 2 — present on v2 sessions (WU-2754). */
726
- lifecycle_role?: string;
727
- /** ADR-014 extension 2 — present on v2 sessions (WU-2754). */
728
- specialty_profile?: string;
729
- display_name?: string;
730
- /** ADR-014 extension 2 — present when session linked to a delegation (WU-2754). */
731
- delegation_id?: string;
732
- /** ADR-014 extension 2 — 'v2' when session was written with the v2 schema (WU-2754). */
733
- schemaVersion?: SessionSchemaVersion;
734
- }
735
-
736
- /**
737
- * Result of ending a session
738
- */
739
- interface EndSessionResult {
740
- ended: boolean;
741
- summary?: SessionSummary;
742
- reason?: string;
743
- }
744
-
745
- /**
746
- * End the current session (called by wu:done)
747
- *
748
- * Unlike endSession in agent-session.ts, this function:
749
- * - Does NOT throw if no active session (returns { ended: false })
750
- * - Returns structured result with summary
751
- * - Supports custom session directory for testing
752
- *
753
- * @param options - Session options
754
- * @returns Session end result
755
- */
756
- export function endSessionForWU(options: EndSessionOptions = {}): EndSessionResult {
757
- const {
758
- wuId,
759
- sessionDir,
760
- workspaceRoot = process.cwd(),
761
- environment = process.env,
762
- controlPlaneSyncPort,
763
- fetchFn = fetch,
764
- } = options;
765
-
766
- const sessDir = sessionDir ?? SESSION_DIR;
767
- const canonicalSessionFile =
768
- typeof wuId === 'string' && wuId.length > 0 ? getCanonicalSessionFilePath(sessDir, wuId) : null;
769
- const compatibilitySessionFile = getSessionFilePath(sessDir);
770
- const session =
771
- (typeof wuId === 'string' && wuId.length > 0
772
- ? resolveSessionForWU(sessDir, wuId)
773
- : resolveCurrentCompatibilitySession(sessDir)) ?? null;
774
-
775
- if (!session) {
776
- return {
777
- ended: false,
778
- reason: 'no_active_session',
779
- };
780
- }
781
-
782
- // Finalize session
783
- session.completed = new Date().toISOString();
784
- releaseDisplayName(workspaceRoot, session.session_id);
785
-
786
- // Build summary for WU YAML
787
- // WU-2754 (ADR-014 extension 2): include the three role axes when the
788
- // session was stamped v2 or otherwise carries them.
789
- const summary: SessionSummary = {
790
- wu_id: session.wu_id,
791
- session_id: session.session_id,
792
- started: session.started,
793
- completed: session.completed,
794
- agent_type: session.agent_type,
795
- context_tier: session.context_tier,
796
- incidents_logged: session.incidents_logged,
797
- incidents_major: session.incidents_major,
798
- ...(session.schemaVersion ? { schemaVersion: session.schemaVersion } : {}),
799
- ...(session.lifecycle_role ? { lifecycle_role: session.lifecycle_role } : {}),
800
- ...(session.specialty_profile ? { specialty_profile: session.specialty_profile } : {}),
801
- ...(session.display_name ? { display_name: session.display_name } : {}),
802
- ...(session.delegation_id ? { delegation_id: session.delegation_id } : {}),
803
- };
804
-
805
- const resolvedCanonicalSessionFile = getCanonicalSessionFilePath(sessDir, session.wu_id);
806
- if (existsSync(resolvedCanonicalSessionFile)) {
807
- unlinkSync(resolvedCanonicalSessionFile);
808
- }
809
-
810
- const compatibilitySession = readSessionFile(compatibilitySessionFile);
811
- if (
812
- compatibilitySession?.session_id === session.session_id &&
813
- existsSync(compatibilitySessionFile)
814
- ) {
815
- unlinkSync(compatibilitySessionFile);
816
- }
817
-
818
- if (canonicalSessionFile && existsSync(canonicalSessionFile)) {
819
- unlinkSync(canonicalSessionFile);
820
- }
821
- refreshCompatibilityPointer(sessDir);
822
-
823
- // WU-2153: Optional control-plane session deregistration.
824
- // Fail-open by design: completion path must not block on remote errors.
825
- const controlPlaneConfig = resolveControlPlaneSessionSyncConfig(workspaceRoot, environment);
826
- if (controlPlaneConfig) {
827
- const deregisterInput: DeregisterSessionInput = {
828
- workspace_id: controlPlaneConfig.workspaceId,
829
- session_id: session.session_id,
830
- ended_at: session.completed,
831
- reason: 'wu_done',
832
- };
833
-
834
- if (controlPlaneSyncPort) {
835
- void controlPlaneSyncPort.deregisterSession(deregisterInput).catch(() => {});
836
- } else {
837
- void postControlPlaneSessionHook(
838
- controlPlaneConfig.endpoint,
839
- controlPlaneConfig.token,
840
- CONTROL_PLANE_DEREGISTER_PATH,
841
- deregisterInput,
842
- fetchFn,
843
- ).catch(() => {});
844
- }
845
- }
846
-
847
- return {
848
- ended: true,
849
- summary,
850
- };
851
- }
852
-
853
- /**
854
- * Options for getting current session
855
- */
856
- interface GetSessionOptions {
857
- wuId?: string;
858
- sessionDir?: string;
859
- }
860
-
861
- /**
862
- * Get the current active session
863
- *
864
- * @param options - Session options
865
- * @returns Session object or null if no active session
866
- */
867
- export function getCurrentSessionForWU(options: GetSessionOptions = {}): SessionFileData | null {
868
- const { wuId, sessionDir } = options;
869
-
870
- const sessDir = sessionDir ?? SESSION_DIR;
871
- if (typeof wuId === 'string' && wuId.length > 0) {
872
- return resolveSessionForWU(sessDir, wuId);
873
- }
874
-
875
- return resolveCurrentCompatibilitySession(sessDir);
876
- }
877
-
878
- /**
879
- * Check if there's an active session for a specific WU
880
- *
881
- * @param wuId - WU ID to check
882
- * @param options - Session options
883
- * @returns True if session exists and matches WU ID
884
- */
885
- export function hasActiveSessionForWU(wuId: string, options: GetSessionOptions = {}): boolean {
886
- const session = getCurrentSessionForWU(options);
887
- return session !== null && session.wu_id === wuId;
888
- }