@lumenflow/cli 5.4.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.
- package/README.md +42 -40
- package/dist/db-journal-recover.js +400 -0
- package/dist/db-journal-recover.js.map +1 -0
- package/dist/docs-sync.js +8 -3
- package/dist/docs-sync.js.map +1 -1
- package/dist/gate-defaults.js +191 -9
- package/dist/gate-defaults.js.map +1 -1
- package/dist/gate-registry.js.map +1 -1
- package/dist/gates/monolithic-file-contention-guard.js +167 -0
- package/dist/gates/monolithic-file-contention-guard.js.map +1 -0
- package/dist/gates/prod-migration-drift.js +207 -0
- package/dist/gates/prod-migration-drift.js.map +1 -0
- package/dist/gates/test-over-deletion-guard.js +255 -0
- package/dist/gates/test-over-deletion-guard.js.map +1 -0
- package/dist/gates-runners.js +401 -2
- package/dist/gates-runners.js.map +1 -1
- package/dist/gates.js +349 -4
- package/dist/gates.js.map +1 -1
- package/dist/lumenflow-setup.js +144 -0
- package/dist/lumenflow-setup.js.map +1 -0
- package/dist/lumenflow-upgrade.js +2 -1
- package/dist/lumenflow-upgrade.js.map +1 -1
- package/dist/mem-create.js +10 -1
- package/dist/mem-create.js.map +1 -1
- package/dist/mem-signal.js +21 -4
- package/dist/mem-signal.js.map +1 -1
- package/dist/metrics-cli.js +19 -2
- package/dist/metrics-cli.js.map +1 -1
- package/dist/metrics-snapshot.js +25 -2
- package/dist/metrics-snapshot.js.map +1 -1
- package/dist/orchestrate-initiative.js +28 -3
- package/dist/orchestrate-initiative.js.map +1 -1
- package/dist/public-manifest.js +17 -0
- package/dist/public-manifest.js.map +1 -1
- package/dist/release.js +53 -18
- package/dist/release.js.map +1 -1
- package/dist/wu-done-gates.js +121 -8
- package/dist/wu-done-gates.js.map +1 -1
- package/dist/wu-done.js +30 -6
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-edit-operations.js +74 -0
- package/dist/wu-edit-operations.js.map +1 -1
- package/dist/wu-edit-validators.js +58 -0
- package/dist/wu-edit-validators.js.map +1 -1
- package/dist/wu-edit.js +106 -4
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-prep.js +132 -8
- package/dist/wu-prep.js.map +1 -1
- package/dist/wu-recover.js +6 -0
- package/dist/wu-recover.js.map +1 -1
- package/dist/wu-release.js +120 -2
- package/dist/wu-release.js.map +1 -1
- package/dist/wu-sizing-validation.js +47 -17
- package/dist/wu-sizing-validation.js.map +1 -1
- package/dist/wu-status.js +33 -0
- package/dist/wu-status.js.map +1 -1
- package/package.json +13 -11
- package/packs/agent-runtime/package.json +1 -1
- package/packs/sidekick/package.json +1 -1
- package/packs/software-delivery/package.json +1 -1
- package/templates/core/AGENTS.md.template +162 -26
- package/templates/core/LUMENFLOW.md.template +381 -70
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +0 -5
- package/templates/core/ai/onboarding/agent-safety-card.md.template +63 -17
- package/templates/core/ai/onboarding/initiative-orchestration.md.template +4 -0
- package/templates/core/ai/onboarding/release-process.md.template +7 -7
- package/templates/core/ai/onboarding/vendor-support.md.template +74 -10
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +1 -1
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +28 -0
- package/packs/agent-runtime/agent-heartbeat.ts +0 -163
- package/packs/agent-runtime/auto-session-integration.ts +0 -888
- package/packs/agent-runtime/capability-factory.ts +0 -104
- package/packs/agent-runtime/constants.ts +0 -21
- package/packs/agent-runtime/delegation-registry-schema.ts +0 -220
- package/packs/agent-runtime/delegation-registry-store.ts +0 -269
- package/packs/agent-runtime/delegation-tree.ts +0 -328
- package/packs/agent-runtime/index.ts +0 -20
- package/packs/agent-runtime/manifest.ts +0 -348
- package/packs/agent-runtime/memory-coordination-contract.ts +0 -86
- package/packs/agent-runtime/orchestration.ts +0 -2027
- package/packs/agent-runtime/pack-registration.ts +0 -110
- package/packs/agent-runtime/policy-factory.ts +0 -165
- package/packs/agent-runtime/remote-controls/index.ts +0 -7
- package/packs/agent-runtime/remote-controls/operations.ts +0 -405
- package/packs/agent-runtime/remote-controls/port.ts +0 -48
- package/packs/agent-runtime/remote-controls/state-store.ts +0 -258
- package/packs/agent-runtime/remote-controls/types.ts +0 -105
- package/packs/agent-runtime/session-schema.ts +0 -467
- package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +0 -793
- package/packs/agent-runtime/tool-impl/index.ts +0 -6
- package/packs/agent-runtime/tool-impl/provider-adapters.ts +0 -1245
- package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +0 -256
- package/packs/agent-runtime/tool-impl/remote-controls.ts +0 -273
- package/packs/agent-runtime/tools/index.ts +0 -4
- package/packs/agent-runtime/tools/types.ts +0 -47
- package/packs/agent-runtime/turn-lifecycle-events.ts +0 -590
- package/packs/agent-runtime/types.ts +0 -128
- package/packs/agent-runtime/vitest.config.ts +0 -11
- package/packs/sidekick/channel-ingress.ts +0 -137
- package/packs/sidekick/constants.ts +0 -10
- package/packs/sidekick/index.ts +0 -8
- package/packs/sidekick/manifest-schema.ts +0 -49
- package/packs/sidekick/manifest.ts +0 -512
- package/packs/sidekick/pack-registration.ts +0 -110
- package/packs/sidekick/policy-factory.ts +0 -38
- package/packs/sidekick/sidekick-events.ts +0 -694
- package/packs/sidekick/src/adapters/cloud-queue.ts +0 -101
- package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +0 -386
- package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +0 -228
- package/packs/sidekick/src/domain/channel.types.ts +0 -64
- package/packs/sidekick/src/ports/channel-bridge.port.ts +0 -92
- package/packs/sidekick/src/routines/commit.ts +0 -74
- package/packs/sidekick/tool-impl/channel-tools.ts +0 -577
- package/packs/sidekick/tool-impl/channel-transports.ts +0 -75
- package/packs/sidekick/tool-impl/index.ts +0 -29
- package/packs/sidekick/tool-impl/memory-tools.ts +0 -290
- package/packs/sidekick/tool-impl/routine-commit.ts +0 -102
- package/packs/sidekick/tool-impl/routine-tools.ts +0 -440
- package/packs/sidekick/tool-impl/runtime-context.ts +0 -28
- package/packs/sidekick/tool-impl/shared.ts +0 -125
- package/packs/sidekick/tool-impl/storage.ts +0 -325
- package/packs/sidekick/tool-impl/system-tools.ts +0 -160
- package/packs/sidekick/tool-impl/task-tools.ts +0 -506
- package/packs/sidekick/tools/channel-tools.ts +0 -53
- package/packs/sidekick/tools/index.ts +0 -9
- package/packs/sidekick/tools/memory-tools.ts +0 -53
- package/packs/sidekick/tools/routine-tools.ts +0 -53
- package/packs/sidekick/tools/system-tools.ts +0 -47
- package/packs/sidekick/tools/task-tools.ts +0 -61
- package/packs/sidekick/tools/types.ts +0 -57
- package/packs/sidekick/vitest.config.ts +0 -11
- package/packs/software-delivery/constants.ts +0 -10
- package/packs/software-delivery/extensions.ts +0 -140
- package/packs/software-delivery/gate-policies.ts +0 -134
- package/packs/software-delivery/index.ts +0 -8
- package/packs/software-delivery/manifest-schema.ts +0 -268
- package/packs/software-delivery/manifest.ts +0 -657
- package/packs/software-delivery/pack-registration.ts +0 -113
- package/packs/software-delivery/src/commands/index.ts +0 -5
- package/packs/software-delivery/src/config/delivery-review-contract.ts +0 -256
- package/packs/software-delivery/src/config/env-accessors.ts +0 -66
- package/packs/software-delivery/src/config/index.ts +0 -8
- package/packs/software-delivery/src/config/normalize-config-keys.ts +0 -9
- package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +0 -460
- package/packs/software-delivery/src/config/workspace-reader.ts +0 -375
- package/packs/software-delivery/src/constants/backlog-patterns.ts +0 -31
- package/packs/software-delivery/src/constants/client-ids.ts +0 -19
- package/packs/software-delivery/src/constants/config-contract.ts +0 -7
- package/packs/software-delivery/src/constants/docs-layout-presets.ts +0 -50
- package/packs/software-delivery/src/constants/duration-constants.ts +0 -20
- package/packs/software-delivery/src/constants/gate-constants.ts +0 -32
- package/packs/software-delivery/src/constants/index.ts +0 -29
- package/packs/software-delivery/src/constants/lock-constants.ts +0 -35
- package/packs/software-delivery/src/constants/object-guards.ts +0 -12
- package/packs/software-delivery/src/constants/section-headings.ts +0 -107
- package/packs/software-delivery/src/constants/wu-cli-constants.ts +0 -488
- package/packs/software-delivery/src/constants/wu-domain-constants.ts +0 -466
- package/packs/software-delivery/src/constants/wu-git-constants.ts +0 -7
- package/packs/software-delivery/src/constants/wu-id-format.ts +0 -327
- package/packs/software-delivery/src/constants/wu-paths-constants.ts +0 -384
- package/packs/software-delivery/src/constants/wu-statuses.ts +0 -287
- package/packs/software-delivery/src/constants/wu-type-helpers.ts +0 -67
- package/packs/software-delivery/src/constants/wu-ui-constants.ts +0 -267
- package/packs/software-delivery/src/constants/wu-validation-constants.ts +0 -73
- package/packs/software-delivery/src/domain/index.ts +0 -5
- package/packs/software-delivery/src/domain/orchestration.constants.ts +0 -166
- package/packs/software-delivery/src/domain/orchestration.schemas.ts +0 -238
- package/packs/software-delivery/src/domain/orchestration.types.ts +0 -176
- package/packs/software-delivery/src/methodology/incremental-test.ts +0 -122
- package/packs/software-delivery/src/methodology/index.ts +0 -6
- package/packs/software-delivery/src/methodology/manual-test-validator.ts +0 -292
- package/packs/software-delivery/src/policy/coverage-gate.ts +0 -270
- package/packs/software-delivery/src/policy/gates-agent-mode.ts +0 -223
- package/packs/software-delivery/src/policy/gates-config-internal.ts +0 -121
- package/packs/software-delivery/src/policy/gates-config.ts +0 -300
- package/packs/software-delivery/src/policy/gates-coverage.ts +0 -356
- package/packs/software-delivery/src/policy/gates-presets.ts +0 -134
- package/packs/software-delivery/src/policy/gates-schemas.ts +0 -173
- package/packs/software-delivery/src/policy/index.ts +0 -22
- package/packs/software-delivery/src/policy/package-manager-resolver.ts +0 -319
- package/packs/software-delivery/src/policy/resolve-policy.ts +0 -601
- package/packs/software-delivery/src/ports/config.ports.ts +0 -90
- package/packs/software-delivery/src/ports/dashboard-renderer.port.ts +0 -125
- package/packs/software-delivery/src/ports/index.ts +0 -10
- package/packs/software-delivery/src/ports/sync-validator.ports.ts +0 -59
- package/packs/software-delivery/src/ports/wu-helpers.ports.ts +0 -168
- package/packs/software-delivery/src/ports/wu-state.ports.ts +0 -241
- package/packs/software-delivery/src/primitives/index.ts +0 -5
- package/packs/software-delivery/src/runtime/index.ts +0 -6
- package/packs/software-delivery/src/runtime/work-classifier.ts +0 -561
- package/packs/software-delivery/src/sandbox/index.ts +0 -10
- package/packs/software-delivery/src/sandbox/sandbox-allowlist.ts +0 -118
- package/packs/software-delivery/src/sandbox/sandbox-backend-linux.ts +0 -88
- package/packs/software-delivery/src/sandbox/sandbox-backend-macos.ts +0 -154
- package/packs/software-delivery/src/sandbox/sandbox-backend-windows.ts +0 -47
- package/packs/software-delivery/src/sandbox/sandbox-profile.ts +0 -153
- package/packs/software-delivery/src/schemas/index.ts +0 -5
- package/packs/software-delivery/src/state/date-utils.ts +0 -158
- package/packs/software-delivery/src/state/index.ts +0 -15
- package/packs/software-delivery/src/state/state-machine.ts +0 -119
- package/packs/software-delivery/src/state/wu-doc-types.ts +0 -51
- package/packs/software-delivery/src/state/wu-paths.ts +0 -381
- package/packs/software-delivery/src/state/wu-schema.ts +0 -1139
- package/packs/software-delivery/src/state/wu-state-schema.ts +0 -255
- package/packs/software-delivery/src/state/wu-yaml.ts +0 -338
- package/packs/software-delivery/tool-impl/agent-tools.ts +0 -263
- package/packs/software-delivery/tool-impl/delegation-tools.ts +0 -66
- package/packs/software-delivery/tool-impl/flow-metrics-tools.ts +0 -219
- package/packs/software-delivery/tool-impl/git-runner.ts +0 -113
- package/packs/software-delivery/tool-impl/git-tools.ts +0 -316
- package/packs/software-delivery/tool-impl/index.ts +0 -15
- package/packs/software-delivery/tool-impl/initiative-orchestration-tools.ts +0 -720
- package/packs/software-delivery/tool-impl/lane-lock.ts +0 -246
- package/packs/software-delivery/tool-impl/memory-tools.ts +0 -470
- package/packs/software-delivery/tool-impl/pending-runtime-tools.ts +0 -21
- package/packs/software-delivery/tool-impl/runtime-cli-adapter.ts +0 -329
- package/packs/software-delivery/tool-impl/runtime-native-tools.ts +0 -687
- package/packs/software-delivery/tool-impl/worker-loader.ts +0 -52
- package/packs/software-delivery/tool-impl/worktree-tools.ts +0 -46
- package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +0 -807
- package/packs/software-delivery/tools/delegation-tools.ts +0 -23
- package/packs/software-delivery/tools/git-tools.ts +0 -55
- package/packs/software-delivery/tools/index.ts +0 -8
- package/packs/software-delivery/tools/lane-lock-tool.ts +0 -37
- package/packs/software-delivery/tools/types.ts +0 -71
- package/packs/software-delivery/tools/worktree-tools.ts +0 -49
- 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
|
-
}
|