@lumenflow/cli 4.24.0 → 5.0.0
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 +54 -52
- package/dist/agent-issues-query.js +10 -2
- package/dist/agent-issues-query.js.map +1 -1
- package/dist/agent-runtime-enrollment-events.js +44 -0
- package/dist/agent-runtime-enrollment-events.js.map +1 -0
- package/dist/agent-session-end.js +47 -0
- package/dist/agent-session-end.js.map +1 -1
- package/dist/agent-session-heartbeat.js +250 -0
- package/dist/agent-session-heartbeat.js.map +1 -0
- package/dist/agent-session.js +299 -5
- package/dist/agent-session.js.map +1 -1
- package/dist/capacity-snapshot-emitter.js +73 -0
- package/dist/capacity-snapshot-emitter.js.map +1 -0
- package/dist/claim-queue.js +276 -0
- package/dist/claim-queue.js.map +1 -0
- package/dist/config-set.js +22 -3
- package/dist/config-set.js.map +1 -1
- package/dist/control-plane-sidecar-runner.js +145 -0
- package/dist/control-plane-sidecar-runner.js.map +1 -0
- package/dist/delegation-list.js +160 -1
- package/dist/delegation-list.js.map +1 -1
- package/dist/delegation-role-resolver.js +69 -0
- package/dist/delegation-role-resolver.js.map +1 -0
- package/dist/docs-generate-pack-reference.js +500 -0
- package/dist/docs-generate-pack-reference.js.map +1 -0
- package/dist/docs-sync.js +116 -1
- package/dist/docs-sync.js.map +1 -1
- package/dist/file-edit.js +28 -8
- package/dist/file-edit.js.map +1 -1
- package/dist/file-write.js +29 -5
- package/dist/file-write.js.map +1 -1
- package/dist/gate-co-change.js +25 -7
- package/dist/gate-co-change.js.map +1 -1
- package/dist/gate-conditional.js +19 -7
- package/dist/gate-conditional.js.map +1 -1
- package/dist/gates-runners.js +42 -33
- package/dist/gates-runners.js.map +1 -1
- package/dist/gates-utils.js +34 -20
- package/dist/gates-utils.js.map +1 -1
- package/dist/gates.js +79 -7
- package/dist/gates.js.map +1 -1
- package/dist/hooks/config-resolver.js +10 -1
- package/dist/hooks/config-resolver.js.map +1 -1
- package/dist/init-package-config.js +1 -1
- package/dist/init-package-config.js.map +1 -1
- package/dist/init-scaffolding.js +5 -1
- package/dist/init-scaffolding.js.map +1 -1
- package/dist/init-templates.js +10 -0
- package/dist/init-templates.js.map +1 -1
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/initiative-create.js +17 -0
- package/dist/initiative-create.js.map +1 -1
- package/dist/initiative-remove-wu.js +17 -3
- package/dist/initiative-remove-wu.js.map +1 -1
- package/dist/kernel-event-sync/emitters.js +104 -0
- package/dist/kernel-event-sync/emitters.js.map +1 -0
- package/dist/kernel-event-sync/index.js +13 -0
- package/dist/kernel-event-sync/index.js.map +1 -0
- package/dist/kernel-event-sync/lifecycle-emitters.js +160 -0
- package/dist/kernel-event-sync/lifecycle-emitters.js.map +1 -0
- package/dist/kernel-event-sync/narrow-emissions.js +89 -0
- package/dist/kernel-event-sync/narrow-emissions.js.map +1 -0
- package/dist/kernel-event-sync/software-delivery-emitters.js +297 -0
- package/dist/kernel-event-sync/software-delivery-emitters.js.map +1 -0
- package/dist/lane-lock.js +14 -1
- package/dist/lane-lock.js.map +1 -1
- package/dist/lane-suggest.js +21 -0
- package/dist/lane-suggest.js.map +1 -1
- package/dist/lumenflow-upgrade.js +7 -5
- package/dist/lumenflow-upgrade.js.map +1 -1
- package/dist/mem-context.js +145 -0
- package/dist/mem-context.js.map +1 -1
- package/dist/mem-create.js +39 -6
- package/dist/mem-create.js.map +1 -1
- package/dist/mem-inbox.js +16 -0
- package/dist/mem-inbox.js.map +1 -1
- package/dist/mem-roster.js +95 -0
- package/dist/mem-roster.js.map +1 -0
- package/dist/mem-signal.js +97 -2
- package/dist/mem-signal.js.map +1 -1
- package/dist/metrics-snapshot.js +3 -2
- package/dist/metrics-snapshot.js.map +1 -1
- package/dist/orchestrate-init-status.js +117 -2
- package/dist/orchestrate-init-status.js.map +1 -1
- package/dist/orchestrate-initiative.js +83 -10
- package/dist/orchestrate-initiative.js.map +1 -1
- package/dist/orchestrate-monitor-quality.js +289 -0
- package/dist/orchestrate-monitor-quality.js.map +1 -0
- package/dist/orchestrate-monitor.js +85 -0
- package/dist/orchestrate-monitor.js.map +1 -1
- package/dist/pack-validate.js +127 -2
- package/dist/pack-validate.js.map +1 -1
- package/dist/plan-create.js +18 -0
- package/dist/plan-create.js.map +1 -1
- package/dist/plan-link.js +13 -0
- package/dist/plan-link.js.map +1 -1
- package/dist/plan-promote.js +14 -0
- package/dist/plan-promote.js.map +1 -1
- package/dist/pre-commit-check.js +4 -3
- package/dist/pre-commit-check.js.map +1 -1
- package/dist/public-manifest.js +17 -3
- package/dist/public-manifest.js.map +1 -1
- package/dist/release.js +10 -10
- package/dist/release.js.map +1 -1
- package/dist/session-cross-link.js +139 -0
- package/dist/session-cross-link.js.map +1 -0
- package/dist/sidecar-manager.js +208 -0
- package/dist/sidecar-manager.js.map +1 -0
- package/dist/state-path-resolvers.js +18 -0
- package/dist/state-path-resolvers.js.map +1 -1
- package/dist/stream-heartbeat.js +151 -0
- package/dist/stream-heartbeat.js.map +1 -0
- package/dist/sync-templates.js +56 -2
- package/dist/sync-templates.js.map +1 -1
- package/dist/wu-block.js +47 -5
- package/dist/wu-block.js.map +1 -1
- package/dist/wu-claim-branch.js +8 -4
- package/dist/wu-claim-branch.js.map +1 -1
- package/dist/wu-claim-state.js +5 -3
- package/dist/wu-claim-state.js.map +1 -1
- package/dist/wu-claim-worktree.js +5 -3
- package/dist/wu-claim-worktree.js.map +1 -1
- package/dist/wu-claim.js +261 -9
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-done-auto-cleanup.js +3 -2
- package/dist/wu-done-auto-cleanup.js.map +1 -1
- package/dist/wu-done-git-ops.js +12 -8
- package/dist/wu-done-git-ops.js.map +1 -1
- package/dist/wu-done-preflight.js +3 -3
- package/dist/wu-done-preflight.js.map +1 -1
- package/dist/wu-done.js +46 -10
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-lifecycle-sync/gate-scope-resolver.js +16 -0
- package/dist/wu-lifecycle-sync/gate-scope-resolver.js.map +1 -0
- package/dist/wu-lifecycle-sync/kernel-event-sync-shim.js +10 -0
- package/dist/wu-lifecycle-sync/kernel-event-sync-shim.js.map +1 -0
- package/dist/wu-prep.js +363 -22
- package/dist/wu-prep.js.map +1 -1
- package/dist/wu-prune.js +68 -27
- package/dist/wu-prune.js.map +1 -1
- package/dist/wu-release.js +34 -3
- package/dist/wu-release.js.map +1 -1
- package/dist/wu-review.js +167 -0
- package/dist/wu-review.js.map +1 -0
- package/dist/wu-spawn-prompt-builders.js +296 -40
- package/dist/wu-spawn-prompt-builders.js.map +1 -1
- package/dist/wu-spawn-strategy-resolver.js +126 -14
- package/dist/wu-spawn-strategy-resolver.js.map +1 -1
- package/dist/wu-unblock.js +52 -22
- package/dist/wu-unblock.js.map +1 -1
- package/package.json +13 -8
- package/packs/agent-runtime/.turbo/turbo-build.log +1 -1
- package/packs/agent-runtime/.turbo/turbo-test.log +25 -0
- package/packs/agent-runtime/.turbo/turbo-typecheck.log +4 -0
- package/packs/agent-runtime/agent-heartbeat.ts +163 -0
- package/packs/agent-runtime/auto-session-integration.ts +874 -0
- package/packs/agent-runtime/delegation-registry-schema.ts +220 -0
- package/packs/agent-runtime/delegation-registry-store.ts +269 -0
- package/packs/agent-runtime/delegation-tree.ts +328 -0
- package/packs/agent-runtime/index.ts +9 -0
- package/packs/agent-runtime/manifest.ts +103 -19
- package/packs/agent-runtime/manifest.yaml +132 -0
- package/packs/agent-runtime/memory-coordination-contract.ts +86 -0
- package/packs/agent-runtime/memory.d.ts +19 -0
- package/packs/agent-runtime/orchestration.ts +238 -23
- package/packs/agent-runtime/package.json +11 -2
- package/packs/agent-runtime/remote-controls/index.ts +7 -0
- package/packs/agent-runtime/remote-controls/operations.ts +399 -0
- package/packs/agent-runtime/remote-controls/port.ts +48 -0
- package/packs/agent-runtime/remote-controls/state-store.ts +258 -0
- package/packs/agent-runtime/remote-controls/types.ts +105 -0
- package/packs/agent-runtime/session-schema.ts +423 -0
- package/packs/agent-runtime/tool-impl/index.ts +1 -0
- package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +252 -0
- package/packs/agent-runtime/tool-impl/remote-controls.ts +273 -0
- package/packs/agent-runtime/tsconfig.json +1 -1
- package/packs/agent-runtime/turn-lifecycle-events.ts +501 -0
- package/packs/sidekick/.lumenflow/state/conductor/outbox/sidekick-events.jsonl +213 -0
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/.turbo/turbo-test.log +25 -0
- package/packs/sidekick/.turbo/turbo-typecheck.log +4 -0
- package/packs/sidekick/channel-ingress.ts +137 -0
- package/packs/sidekick/manifest.ts +74 -0
- package/packs/sidekick/manifest.yaml +88 -0
- package/packs/sidekick/package.json +3 -1
- package/packs/sidekick/sidekick-events.ts +517 -0
- package/packs/sidekick/src/adapters/cloud-queue.ts +101 -0
- package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +378 -0
- package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +224 -0
- package/packs/sidekick/src/domain/channel.types.ts +84 -0
- package/packs/sidekick/src/ports/channel-bridge.port.ts +75 -0
- package/packs/sidekick/src/routines/commit.ts +74 -0
- package/packs/sidekick/tool-impl/channel-tools.ts +47 -0
- package/packs/sidekick/tool-impl/memory-tools.ts +17 -0
- package/packs/sidekick/tool-impl/routine-commit.ts +102 -0
- package/packs/sidekick/tool-impl/routine-tools.ts +67 -7
- package/packs/sidekick/tool-impl/runtime-context.ts +4 -0
- package/packs/sidekick/tool-impl/storage.ts +3 -0
- package/packs/sidekick/tool-impl/system-tools.ts +7 -0
- package/packs/sidekick/tool-impl/task-tools.ts +46 -0
- package/packs/sidekick/tsconfig.json +1 -1
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/.turbo/turbo-test.log +63 -0
- package/packs/software-delivery/.turbo/turbo-typecheck.log +4 -0
- package/packs/software-delivery/manifest-schema.ts +30 -0
- package/packs/software-delivery/manifest.ts +99 -1
- package/packs/software-delivery/manifest.yaml +46 -0
- package/packs/software-delivery/package.json +88 -3
- package/packs/software-delivery/src/commands/index.ts +5 -0
- package/packs/software-delivery/src/config/delivery-review-contract.ts +20 -0
- package/packs/software-delivery/src/config/env-accessors.ts +19 -0
- package/packs/software-delivery/src/config/index.ts +8 -0
- package/packs/software-delivery/src/config/normalize-config-keys.ts +19 -0
- package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +436 -0
- package/packs/software-delivery/src/config/workspace-reader.ts +310 -0
- package/packs/software-delivery/src/constants/backlog-patterns.ts +31 -0
- package/packs/software-delivery/src/constants/client-ids.ts +19 -0
- package/packs/software-delivery/src/constants/config-contract.ts +7 -0
- package/packs/software-delivery/src/constants/docs-layout-presets.ts +50 -0
- package/packs/software-delivery/src/constants/duration-constants.ts +20 -0
- package/packs/software-delivery/src/constants/gate-constants.ts +32 -0
- package/packs/software-delivery/src/constants/index.ts +29 -0
- package/packs/software-delivery/src/constants/lock-constants.ts +35 -0
- package/packs/software-delivery/src/constants/object-guards.ts +12 -0
- package/packs/software-delivery/src/constants/section-headings.ts +107 -0
- package/packs/software-delivery/src/constants/wu-cli-constants.ts +485 -0
- package/packs/software-delivery/src/constants/wu-domain-constants.ts +466 -0
- package/packs/software-delivery/src/constants/wu-git-constants.ts +7 -0
- package/packs/software-delivery/src/constants/wu-id-format.ts +327 -0
- package/packs/software-delivery/src/constants/wu-paths-constants.ts +358 -0
- package/packs/software-delivery/src/constants/wu-statuses.ts +287 -0
- package/packs/software-delivery/src/constants/wu-type-helpers.ts +67 -0
- package/packs/software-delivery/src/constants/wu-ui-constants.ts +267 -0
- package/packs/software-delivery/src/constants/wu-validation-constants.ts +73 -0
- package/packs/software-delivery/src/domain/index.ts +5 -0
- package/packs/software-delivery/src/domain/orchestration.constants.ts +168 -0
- package/packs/software-delivery/src/domain/orchestration.schemas.ts +239 -0
- package/packs/software-delivery/src/domain/orchestration.types.ts +178 -0
- package/packs/software-delivery/src/methodology/incremental-test.ts +90 -0
- package/packs/software-delivery/src/methodology/index.ts +6 -0
- package/packs/software-delivery/src/methodology/manual-test-validator.ts +292 -0
- package/packs/software-delivery/src/policy/coverage-gate.ts +270 -0
- package/packs/software-delivery/src/policy/gates-agent-mode.ts +223 -0
- package/packs/software-delivery/src/policy/gates-config-internal.ts +121 -0
- package/packs/software-delivery/src/policy/gates-config.ts +293 -0
- package/packs/software-delivery/src/policy/gates-coverage.ts +247 -0
- package/packs/software-delivery/src/policy/gates-presets.ts +134 -0
- package/packs/software-delivery/src/policy/gates-schemas.ts +173 -0
- package/packs/software-delivery/src/policy/index.ts +22 -0
- package/packs/software-delivery/src/policy/package-manager-resolver.ts +319 -0
- package/packs/software-delivery/src/policy/resolve-policy.ts +518 -0
- package/packs/software-delivery/src/ports/config.ports.ts +90 -0
- package/packs/software-delivery/src/ports/dashboard-renderer.port.ts +125 -0
- package/packs/software-delivery/src/ports/index.ts +10 -0
- package/packs/software-delivery/src/ports/sync-validator.ports.ts +59 -0
- package/packs/software-delivery/src/ports/wu-helpers.ports.ts +168 -0
- package/packs/software-delivery/src/ports/wu-state.ports.ts +241 -0
- package/packs/software-delivery/src/primitives/index.ts +5 -0
- package/packs/software-delivery/src/runtime/index.ts +6 -0
- package/packs/software-delivery/src/runtime/work-classifier.ts +561 -0
- package/packs/software-delivery/src/sandbox/index.ts +10 -0
- package/packs/software-delivery/src/sandbox/sandbox-allowlist.ts +118 -0
- package/packs/software-delivery/src/sandbox/sandbox-backend-linux.ts +88 -0
- package/packs/software-delivery/src/sandbox/sandbox-backend-macos.ts +154 -0
- package/packs/software-delivery/src/sandbox/sandbox-backend-windows.ts +47 -0
- package/packs/software-delivery/src/sandbox/sandbox-profile.ts +153 -0
- package/packs/software-delivery/src/schemas/index.ts +5 -0
- package/packs/software-delivery/src/state/date-utils.ts +158 -0
- package/packs/software-delivery/src/state/index.ts +15 -0
- package/packs/software-delivery/src/state/state-machine.ts +119 -0
- package/packs/software-delivery/src/state/wu-doc-types.ts +51 -0
- package/packs/software-delivery/src/state/wu-paths.ts +381 -0
- package/packs/software-delivery/src/state/wu-schema.ts +1139 -0
- package/packs/software-delivery/src/state/wu-state-schema.ts +255 -0
- package/packs/software-delivery/src/state/wu-yaml.ts +338 -0
- package/packs/software-delivery/src/types.d.ts +16 -0
- package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +18 -0
- package/packs/software-delivery/tsconfig.json +28 -2
- package/templates/core/AGENTS.md.template +76 -17
- package/templates/core/LUMENFLOW.md.template +265 -66
- package/templates/core/_frameworks/lumenflow/wu-sizing-guide.md.template +180 -116
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +26 -8
- package/templates/core/ai/onboarding/existing-project-bootstrap.md.template +171 -0
- package/templates/core/ai/onboarding/first-15-mins.md.template +3 -1
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +1 -1
- package/templates/core/ai/onboarding/initiative-orchestration.md.template +46 -30
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +36 -33
- package/templates/core/ai/onboarding/release-process.md.template +8 -7
- package/templates/core/ai/onboarding/starting-prompt.md.template +2 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +62 -0
- package/templates/vendors/claude/.claude/CLAUDE.md.template +29 -54
- package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +24 -52
- package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +24 -52
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// WU-2733 (INIT-060 Phase 3, ADR-013 §1 + §6):
|
|
5
|
+
// Real kernel-side implementations for the six remote-control
|
|
6
|
+
// operations. Each operation:
|
|
7
|
+
// 1. Loads/mutates RemoteControlSessionState via the sidecar store
|
|
8
|
+
// (so state survives process restarts).
|
|
9
|
+
// 2. Emits the ADR-013-governed event via the supplied sink
|
|
10
|
+
// (turn_aborted / autonomy_changed / tool_called).
|
|
11
|
+
// 3. Returns a result that adheres to RemoteControlPort.
|
|
12
|
+
//
|
|
13
|
+
// No auto-wu:recover. recovery_action is always a hint; ADR-013 §1
|
|
14
|
+
// safety lock is preserved.
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import {
|
|
18
|
+
buildToolCalledEvent,
|
|
19
|
+
buildTurnAbortedEvent,
|
|
20
|
+
buildAutonomyChangedEvent,
|
|
21
|
+
emitAgentRuntimeEvent,
|
|
22
|
+
type AgentRuntimeCleanupStatus,
|
|
23
|
+
type AgentRuntimeEventSink,
|
|
24
|
+
} from '../turn-lifecycle-events.js';
|
|
25
|
+
import {
|
|
26
|
+
AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES,
|
|
27
|
+
type AbortTurnInvocationResult,
|
|
28
|
+
type AgentRuntimeRemoteControlToolName,
|
|
29
|
+
type RemoteControlInvocationResult,
|
|
30
|
+
} from './types.js';
|
|
31
|
+
import type { AbortTurnInvocationInput, RemoteControlInvocationInput } from './port.js';
|
|
32
|
+
import {
|
|
33
|
+
AGENT_RUNTIME_AUTONOMY_LEVELS,
|
|
34
|
+
AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES,
|
|
35
|
+
type AgentRuntimeAutonomyLevel,
|
|
36
|
+
type RemoteControlStateStore,
|
|
37
|
+
} from './state-store.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Observed turn index emitted in remote-control tool_called events.
|
|
41
|
+
* The real kernel runtime increments a per-turn counter; remote-control
|
|
42
|
+
* invocations happen OUTSIDE an active turn (that is precisely their
|
|
43
|
+
* purpose), so we emit 0 to signal "not tied to a turn index". This
|
|
44
|
+
* mirrors the mock adapter's constant for wire parity.
|
|
45
|
+
*/
|
|
46
|
+
const REMOTE_CONTROL_TURN_INDEX = 0;
|
|
47
|
+
|
|
48
|
+
export interface RemoteControlOperationContext {
|
|
49
|
+
store: RemoteControlStateStore;
|
|
50
|
+
eventSink?: AgentRuntimeEventSink;
|
|
51
|
+
now: () => string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveSessionId(input: string | undefined): string | null {
|
|
55
|
+
if (input !== undefined && input.trim().length > 0) {
|
|
56
|
+
return input;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function emitToolCalled(
|
|
62
|
+
ctx: RemoteControlOperationContext,
|
|
63
|
+
toolName: AgentRuntimeRemoteControlToolName,
|
|
64
|
+
sessionId: string,
|
|
65
|
+
): void {
|
|
66
|
+
emitAgentRuntimeEvent(
|
|
67
|
+
ctx.eventSink,
|
|
68
|
+
buildToolCalledEvent({
|
|
69
|
+
session_id: sessionId,
|
|
70
|
+
turn_index: REMOTE_CONTROL_TURN_INDEX,
|
|
71
|
+
tool_name: toolName,
|
|
72
|
+
tool_call_id: randomUUID(),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requireSessionId(
|
|
78
|
+
toolName: AgentRuntimeRemoteControlToolName,
|
|
79
|
+
input: string | undefined,
|
|
80
|
+
): string {
|
|
81
|
+
const resolved = resolveSessionId(input);
|
|
82
|
+
if (!resolved) {
|
|
83
|
+
throw new Error(`Remote-control tool "${toolName}" requires a non-empty session_id input.`);
|
|
84
|
+
}
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// pause_turn
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Soft-pause marker honoured at the next checkpoint. The running turn
|
|
94
|
+
* (if any) observes the marker via a state load and suspends gracefully.
|
|
95
|
+
* Re-pausing an already-paused session is idempotent — we refresh the
|
|
96
|
+
* reason+timestamp without toggling.
|
|
97
|
+
*/
|
|
98
|
+
export async function pauseTurn(
|
|
99
|
+
input: RemoteControlInvocationInput & { reason?: string | null },
|
|
100
|
+
ctx: RemoteControlOperationContext,
|
|
101
|
+
): Promise<RemoteControlInvocationResult> {
|
|
102
|
+
const sessionId = requireSessionId(
|
|
103
|
+
AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.PAUSE_TURN,
|
|
104
|
+
input.session_id,
|
|
105
|
+
);
|
|
106
|
+
const now = ctx.now();
|
|
107
|
+
const state = await ctx.store.loadOrDefault(sessionId, now);
|
|
108
|
+
const reason = typeof input.reason === 'string' ? input.reason : null;
|
|
109
|
+
await ctx.store.save({
|
|
110
|
+
...state,
|
|
111
|
+
pause: {
|
|
112
|
+
paused: true,
|
|
113
|
+
paused_at: now,
|
|
114
|
+
resumed_at: null,
|
|
115
|
+
reason,
|
|
116
|
+
},
|
|
117
|
+
updated_at: now,
|
|
118
|
+
});
|
|
119
|
+
emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.PAUSE_TURN, sessionId);
|
|
120
|
+
return {
|
|
121
|
+
status: 'ok',
|
|
122
|
+
tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.PAUSE_TURN,
|
|
123
|
+
session_id: sessionId,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// resume_workflow
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Clears the pause marker so the next checkpoint observer advances the
|
|
133
|
+
* workflow. Does NOT re-enter the agent loop — the pauser/scheduler
|
|
134
|
+
* resumes the session on its next poll. Emits tool_called for audit.
|
|
135
|
+
*/
|
|
136
|
+
export async function resumeWorkflow(
|
|
137
|
+
input: RemoteControlInvocationInput,
|
|
138
|
+
ctx: RemoteControlOperationContext,
|
|
139
|
+
): Promise<RemoteControlInvocationResult> {
|
|
140
|
+
const sessionId = requireSessionId(
|
|
141
|
+
AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.RESUME_WORKFLOW,
|
|
142
|
+
input.session_id,
|
|
143
|
+
);
|
|
144
|
+
const now = ctx.now();
|
|
145
|
+
const state = await ctx.store.loadOrDefault(sessionId, now);
|
|
146
|
+
await ctx.store.save({
|
|
147
|
+
...state,
|
|
148
|
+
pause: {
|
|
149
|
+
paused: false,
|
|
150
|
+
paused_at: state.pause.paused_at,
|
|
151
|
+
resumed_at: now,
|
|
152
|
+
reason: state.pause.reason,
|
|
153
|
+
},
|
|
154
|
+
updated_at: now,
|
|
155
|
+
});
|
|
156
|
+
emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.RESUME_WORKFLOW, sessionId);
|
|
157
|
+
return {
|
|
158
|
+
status: 'ok',
|
|
159
|
+
tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.RESUME_WORKFLOW,
|
|
160
|
+
session_id: sessionId,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// abort_turn (ADR-013 §1)
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Hard-aborts the current turn and records the cleanup_status +
|
|
170
|
+
* recovery_action per ADR-013 §1:
|
|
171
|
+
* - 'clean' — no prior mutations; recovery_action = null
|
|
172
|
+
* - 'partial' — inflight approvals existed; recovery_action
|
|
173
|
+
* is a prose hint
|
|
174
|
+
* - 'needs_recovery' — abort interrupted a mid-flight approval with
|
|
175
|
+
* the abort ordered while paused; operator must
|
|
176
|
+
* run `wu:recover`
|
|
177
|
+
*
|
|
178
|
+
* NEVER auto-triggers wu:recover. The returned recovery_action is a
|
|
179
|
+
* hint cloud renders in the conductor UI (§1 safety lock).
|
|
180
|
+
*/
|
|
181
|
+
export async function abortTurn(
|
|
182
|
+
input: AbortTurnInvocationInput,
|
|
183
|
+
ctx: RemoteControlOperationContext,
|
|
184
|
+
): Promise<AbortTurnInvocationResult> {
|
|
185
|
+
const sessionId = requireSessionId(
|
|
186
|
+
AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ABORT_TURN,
|
|
187
|
+
input.session_id,
|
|
188
|
+
);
|
|
189
|
+
const now = ctx.now();
|
|
190
|
+
const state = await ctx.store.loadOrDefault(sessionId, now);
|
|
191
|
+
const { cleanup_status, recovery_action } = classifyAbort(state);
|
|
192
|
+
const reason =
|
|
193
|
+
typeof input.reason === 'string' && input.reason.trim().length > 0
|
|
194
|
+
? input.reason
|
|
195
|
+
: 'operator-requested';
|
|
196
|
+
await ctx.store.save({
|
|
197
|
+
...state,
|
|
198
|
+
abort: {
|
|
199
|
+
aborted: true,
|
|
200
|
+
aborted_at: now,
|
|
201
|
+
cleanup_status,
|
|
202
|
+
recovery_action,
|
|
203
|
+
reason,
|
|
204
|
+
},
|
|
205
|
+
pause: state.pause,
|
|
206
|
+
updated_at: now,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ADR-013 §1: emit turn_aborted carrying the cleanup contract.
|
|
210
|
+
emitAgentRuntimeEvent(
|
|
211
|
+
ctx.eventSink,
|
|
212
|
+
buildTurnAbortedEvent({
|
|
213
|
+
session_id: sessionId,
|
|
214
|
+
turn_index: REMOTE_CONTROL_TURN_INDEX,
|
|
215
|
+
cleanup_status,
|
|
216
|
+
recovery_action,
|
|
217
|
+
reason,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
// ADR-013 §6: emit tool_called for audit of the governance surface.
|
|
221
|
+
emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ABORT_TURN, sessionId);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
status: 'ok',
|
|
225
|
+
tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ABORT_TURN,
|
|
226
|
+
session_id: sessionId,
|
|
227
|
+
cleanup_status,
|
|
228
|
+
recovery_action,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface AbortClassification {
|
|
233
|
+
cleanup_status: AgentRuntimeCleanupStatus;
|
|
234
|
+
recovery_action: string | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Decide cleanup_status per ADR-013 §1 abort table.
|
|
239
|
+
*
|
|
240
|
+
* - Fresh/unused session state → 'clean'; no mutations occurred.
|
|
241
|
+
* - Paused session with ≥1 inflight approval → 'needs_recovery'; state
|
|
242
|
+
* may be inconsistent because an approval was mid-flight when pause
|
|
243
|
+
* landed and the operator ordered an abort on top.
|
|
244
|
+
* - Either "paused" OR "inflight approvals" alone → 'partial'; the
|
|
245
|
+
* session is self-consistent but has uncommitted work.
|
|
246
|
+
*/
|
|
247
|
+
function classifyAbort(state: {
|
|
248
|
+
pause: { paused: boolean };
|
|
249
|
+
inflight_approvals: readonly unknown[];
|
|
250
|
+
}): AbortClassification {
|
|
251
|
+
const hasInflight = state.inflight_approvals.length > 0;
|
|
252
|
+
const isPaused = state.pause.paused;
|
|
253
|
+
|
|
254
|
+
if (!hasInflight && !isPaused) {
|
|
255
|
+
return { cleanup_status: 'clean', recovery_action: null };
|
|
256
|
+
}
|
|
257
|
+
if (hasInflight && isPaused) {
|
|
258
|
+
return {
|
|
259
|
+
cleanup_status: 'needs_recovery',
|
|
260
|
+
recovery_action:
|
|
261
|
+
'Run `pnpm wu:recover --session ${session}` to reconcile the paused session with pending approval state.',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (hasInflight) {
|
|
265
|
+
return {
|
|
266
|
+
cleanup_status: 'partial',
|
|
267
|
+
recovery_action:
|
|
268
|
+
'Review inflight approvals in the conductor UI; re-run `wu:prep` after the approval is cleared.',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
cleanup_status: 'partial',
|
|
273
|
+
recovery_action:
|
|
274
|
+
'Session was paused when the abort landed; review the paused continuation before resuming.',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// elevate_autonomy / lower_autonomy
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
function autonomyIndex(level: AgentRuntimeAutonomyLevel): number {
|
|
283
|
+
return AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES.indexOf(level);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function nextAutonomyLevel(
|
|
287
|
+
current: AgentRuntimeAutonomyLevel,
|
|
288
|
+
direction: 'elevate' | 'lower',
|
|
289
|
+
): AgentRuntimeAutonomyLevel {
|
|
290
|
+
const index = autonomyIndex(current);
|
|
291
|
+
const delta = direction === 'elevate' ? 1 : -1;
|
|
292
|
+
const candidate = index + delta;
|
|
293
|
+
if (candidate < 0 || candidate >= AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES.length) {
|
|
294
|
+
return current;
|
|
295
|
+
}
|
|
296
|
+
return AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES[candidate] ?? current;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function transitionAutonomy(
|
|
300
|
+
toolName: AgentRuntimeRemoteControlToolName,
|
|
301
|
+
direction: 'elevate' | 'lower',
|
|
302
|
+
input: RemoteControlInvocationInput & { reason?: string | null },
|
|
303
|
+
ctx: RemoteControlOperationContext,
|
|
304
|
+
): Promise<RemoteControlInvocationResult> {
|
|
305
|
+
const sessionId = requireSessionId(toolName, input.session_id);
|
|
306
|
+
const now = ctx.now();
|
|
307
|
+
const state = await ctx.store.loadOrDefault(sessionId, now);
|
|
308
|
+
const previousLevel = state.autonomy_level;
|
|
309
|
+
const newLevel = nextAutonomyLevel(previousLevel, direction);
|
|
310
|
+
await ctx.store.save({
|
|
311
|
+
...state,
|
|
312
|
+
autonomy_level: newLevel,
|
|
313
|
+
updated_at: now,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// §6 governance — record BOTH autonomy_changed AND tool_called.
|
|
317
|
+
emitAgentRuntimeEvent(
|
|
318
|
+
ctx.eventSink,
|
|
319
|
+
buildAutonomyChangedEvent({
|
|
320
|
+
session_id: sessionId,
|
|
321
|
+
previous_level: previousLevel,
|
|
322
|
+
new_level: newLevel,
|
|
323
|
+
direction,
|
|
324
|
+
reason: typeof input.reason === 'string' ? input.reason : null,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
emitToolCalled(ctx, toolName, sessionId);
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
status: 'ok',
|
|
331
|
+
tool_name: toolName,
|
|
332
|
+
session_id: sessionId,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function elevateAutonomy(
|
|
337
|
+
input: RemoteControlInvocationInput & { reason?: string | null },
|
|
338
|
+
ctx: RemoteControlOperationContext,
|
|
339
|
+
): Promise<RemoteControlInvocationResult> {
|
|
340
|
+
return transitionAutonomy(
|
|
341
|
+
AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ELEVATE_AUTONOMY,
|
|
342
|
+
'elevate',
|
|
343
|
+
input,
|
|
344
|
+
ctx,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function lowerAutonomy(
|
|
349
|
+
input: RemoteControlInvocationInput & { reason?: string | null },
|
|
350
|
+
ctx: RemoteControlOperationContext,
|
|
351
|
+
): Promise<RemoteControlInvocationResult> {
|
|
352
|
+
return transitionAutonomy(
|
|
353
|
+
AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.LOWER_AUTONOMY,
|
|
354
|
+
'lower',
|
|
355
|
+
input,
|
|
356
|
+
ctx,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// approve_inflight
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Resolves the oldest pending in-flight approval for a session. The
|
|
366
|
+
* approval queue lives inside RemoteControlSessionState; workflow-level
|
|
367
|
+
* approval gating remains the orchestration module's responsibility —
|
|
368
|
+
* this tool only clears the queue entry so the workflow observer sees
|
|
369
|
+
* the approval as satisfied.
|
|
370
|
+
*
|
|
371
|
+
* If there are no pending approvals, we still emit tool_called and
|
|
372
|
+
* return `status: 'ok'` — the operator's click is auditable even when
|
|
373
|
+
* it was a no-op race against an auto-resolved approval.
|
|
374
|
+
*/
|
|
375
|
+
export async function approveInflight(
|
|
376
|
+
input: RemoteControlInvocationInput,
|
|
377
|
+
ctx: RemoteControlOperationContext,
|
|
378
|
+
): Promise<RemoteControlInvocationResult> {
|
|
379
|
+
const sessionId = requireSessionId(
|
|
380
|
+
AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.APPROVE_INFLIGHT,
|
|
381
|
+
input.session_id,
|
|
382
|
+
);
|
|
383
|
+
const now = ctx.now();
|
|
384
|
+
const state = await ctx.store.loadOrDefault(sessionId, now);
|
|
385
|
+
const [, ...remaining] = state.inflight_approvals;
|
|
386
|
+
await ctx.store.save({
|
|
387
|
+
...state,
|
|
388
|
+
inflight_approvals: remaining,
|
|
389
|
+
updated_at: now,
|
|
390
|
+
});
|
|
391
|
+
emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.APPROVE_INFLIGHT, sessionId);
|
|
392
|
+
return {
|
|
393
|
+
status: 'ok',
|
|
394
|
+
tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.APPROVE_INFLIGHT,
|
|
395
|
+
session_id: sessionId,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export { AGENT_RUNTIME_AUTONOMY_LEVELS };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// WU-2732 (INIT-060 Phase 3, ADR-013 §6 governance surface):
|
|
5
|
+
// Port interface for the six remote-control tools. Both the mock
|
|
6
|
+
// tracer-bullet adapter (this WU) and the real kernel-side adapter
|
|
7
|
+
// (WU-2733) implement this port — so the real impl lands without
|
|
8
|
+
// changing the wire contract. This mirrors ADR-013's ChannelBridge
|
|
9
|
+
// port pattern (single port, multiple adapters).
|
|
10
|
+
|
|
11
|
+
import type { AbortTurnInvocationResult, RemoteControlInvocationResult } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Minimum input shape every remote-control tool accepts. The session
|
|
15
|
+
* identifier is the load-bearing field: it pins which agent-runtime
|
|
16
|
+
* session the command targets. Real impls in WU-2733 will extend this
|
|
17
|
+
* with per-tool specific fields (e.g. autonomy level for elevate);
|
|
18
|
+
* extensions MUST be additive to preserve the wire contract.
|
|
19
|
+
*/
|
|
20
|
+
export interface RemoteControlInvocationInput {
|
|
21
|
+
session_id?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AbortTurnInvocationInput extends RemoteControlInvocationInput {
|
|
25
|
+
/**
|
|
26
|
+
* Operator-supplied reason surfaced in the emitted
|
|
27
|
+
* `agent-runtime:turn_aborted` event in the real impl. The mock
|
|
28
|
+
* tracer-bullet records this for round-trip verification but does
|
|
29
|
+
* not emit the event (WU-2733 wires the real emission per
|
|
30
|
+
* ADR-013 §1 abort table).
|
|
31
|
+
*/
|
|
32
|
+
reason?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Remote-control port. The six methods correspond 1:1 to the six
|
|
37
|
+
* manifest-declared tool names. Keeping them as separate methods (not
|
|
38
|
+
* a single `invoke(name, input)` dispatcher) lets TypeScript enforce
|
|
39
|
+
* per-tool input/output types at the call site.
|
|
40
|
+
*/
|
|
41
|
+
export interface RemoteControlPort {
|
|
42
|
+
resumeWorkflow(input: RemoteControlInvocationInput): Promise<RemoteControlInvocationResult>;
|
|
43
|
+
pauseTurn(input: RemoteControlInvocationInput): Promise<RemoteControlInvocationResult>;
|
|
44
|
+
abortTurn(input: AbortTurnInvocationInput): Promise<AbortTurnInvocationResult>;
|
|
45
|
+
elevateAutonomy(input: RemoteControlInvocationInput): Promise<RemoteControlInvocationResult>;
|
|
46
|
+
lowerAutonomy(input: RemoteControlInvocationInput): Promise<RemoteControlInvocationResult>;
|
|
47
|
+
approveInflight(input: RemoteControlInvocationInput): Promise<RemoteControlInvocationResult>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// WU-2733 (INIT-060 Phase 3, ADR-013 §1 abort + §6 governance):
|
|
5
|
+
// Sidecar persistence for remote-control session state (pause marker,
|
|
6
|
+
// autonomy level, abort record, inflight approval handle). Kept
|
|
7
|
+
// separate from the core orchestration workflow state store so the
|
|
8
|
+
// existing strict schema validator doesn't need schema-version bumps
|
|
9
|
+
// and so remote-control operations remain independently observable
|
|
10
|
+
// via a dedicated on-disk artefact (`.agent-runtime/remote-controls/`).
|
|
11
|
+
|
|
12
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const REMOTE_CONTROL_STATE_DIRECTORY = path.join('.agent-runtime', 'remote-controls');
|
|
16
|
+
|
|
17
|
+
/** Canonical autonomy bands. Declaration order = low→high. */
|
|
18
|
+
export const AGENT_RUNTIME_AUTONOMY_LEVELS = {
|
|
19
|
+
MANUAL: 'manual',
|
|
20
|
+
ASSISTED: 'assisted',
|
|
21
|
+
AUTO: 'auto',
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export type AgentRuntimeAutonomyLevel =
|
|
25
|
+
(typeof AGENT_RUNTIME_AUTONOMY_LEVELS)[keyof typeof AGENT_RUNTIME_AUTONOMY_LEVELS];
|
|
26
|
+
|
|
27
|
+
export const AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES = [
|
|
28
|
+
AGENT_RUNTIME_AUTONOMY_LEVELS.MANUAL,
|
|
29
|
+
AGENT_RUNTIME_AUTONOMY_LEVELS.ASSISTED,
|
|
30
|
+
AGENT_RUNTIME_AUTONOMY_LEVELS.AUTO,
|
|
31
|
+
] as const satisfies readonly AgentRuntimeAutonomyLevel[];
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_AUTONOMY_LEVEL: AgentRuntimeAutonomyLevel =
|
|
34
|
+
AGENT_RUNTIME_AUTONOMY_LEVELS.ASSISTED;
|
|
35
|
+
|
|
36
|
+
export interface RemoteControlPauseState {
|
|
37
|
+
paused: boolean;
|
|
38
|
+
paused_at: string | null;
|
|
39
|
+
resumed_at: string | null;
|
|
40
|
+
reason: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RemoteControlAbortState {
|
|
44
|
+
aborted: boolean;
|
|
45
|
+
aborted_at: string | null;
|
|
46
|
+
cleanup_status: 'clean' | 'partial' | 'needs_recovery' | null;
|
|
47
|
+
recovery_action: string | null;
|
|
48
|
+
reason: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RemoteControlInflightApproval {
|
|
52
|
+
request_id: string;
|
|
53
|
+
tool_name: string;
|
|
54
|
+
requested_at: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RemoteControlSessionState {
|
|
58
|
+
schema_version: 1;
|
|
59
|
+
session_id: string;
|
|
60
|
+
autonomy_level: AgentRuntimeAutonomyLevel;
|
|
61
|
+
pause: RemoteControlPauseState;
|
|
62
|
+
abort: RemoteControlAbortState;
|
|
63
|
+
inflight_approvals: RemoteControlInflightApproval[];
|
|
64
|
+
updated_at: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SCHEMA_VERSION_V1 = 1 as const;
|
|
68
|
+
|
|
69
|
+
export interface RemoteControlStateStore {
|
|
70
|
+
load(sessionId: string): Promise<RemoteControlSessionState | null>;
|
|
71
|
+
save(state: RemoteControlSessionState): Promise<void>;
|
|
72
|
+
loadOrDefault(sessionId: string, now: string): Promise<RemoteControlSessionState>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CreateRemoteControlStateStoreInput {
|
|
76
|
+
workspaceRoot: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createRemoteControlStateStore(
|
|
80
|
+
input: CreateRemoteControlStateStoreInput,
|
|
81
|
+
): RemoteControlStateStore {
|
|
82
|
+
const stateRoot = path.join(input.workspaceRoot, REMOTE_CONTROL_STATE_DIRECTORY);
|
|
83
|
+
|
|
84
|
+
async function load(sessionId: string): Promise<RemoteControlSessionState | null> {
|
|
85
|
+
const filePath = sessionFile(stateRoot, sessionId);
|
|
86
|
+
try {
|
|
87
|
+
const raw = await readFile(filePath, 'utf8');
|
|
88
|
+
return parseState(JSON.parse(raw), filePath);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
91
|
+
if (nodeError.code === 'ENOENT') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function save(state: RemoteControlSessionState): Promise<void> {
|
|
99
|
+
const filePath = sessionFile(stateRoot, state.session_id);
|
|
100
|
+
await mkdir(stateRoot, { recursive: true });
|
|
101
|
+
await writeFile(filePath, JSON.stringify(state), 'utf8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function loadOrDefault(sessionId: string, now: string): Promise<RemoteControlSessionState> {
|
|
105
|
+
const existing = await load(sessionId);
|
|
106
|
+
if (existing) {
|
|
107
|
+
return existing;
|
|
108
|
+
}
|
|
109
|
+
return buildDefaultState(sessionId, now);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { load, save, loadOrDefault };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildDefaultState(sessionId: string, now: string): RemoteControlSessionState {
|
|
116
|
+
return {
|
|
117
|
+
schema_version: SCHEMA_VERSION_V1,
|
|
118
|
+
session_id: sessionId,
|
|
119
|
+
autonomy_level: DEFAULT_AUTONOMY_LEVEL,
|
|
120
|
+
pause: {
|
|
121
|
+
paused: false,
|
|
122
|
+
paused_at: null,
|
|
123
|
+
resumed_at: null,
|
|
124
|
+
reason: null,
|
|
125
|
+
},
|
|
126
|
+
abort: {
|
|
127
|
+
aborted: false,
|
|
128
|
+
aborted_at: null,
|
|
129
|
+
cleanup_status: null,
|
|
130
|
+
recovery_action: null,
|
|
131
|
+
reason: null,
|
|
132
|
+
},
|
|
133
|
+
inflight_approvals: [],
|
|
134
|
+
updated_at: now,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sessionFile(stateRoot: string, sessionId: string): string {
|
|
139
|
+
return path.join(stateRoot, `${sessionId}.json`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseState(value: unknown, filePath: string): RemoteControlSessionState {
|
|
143
|
+
if (value === null || typeof value !== 'object') {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Failed to parse remote-control state at ${filePath}: expected an object payload.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const record = value as Record<string, unknown>;
|
|
149
|
+
if (record.schema_version !== SCHEMA_VERSION_V1) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Failed to parse remote-control state at ${filePath}: unsupported schema_version "${String(record.schema_version)}".`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const sessionId = requireString(record.session_id, `${filePath}.session_id`);
|
|
155
|
+
const autonomyLevel = requireAutonomyLevel(record.autonomy_level, `${filePath}.autonomy_level`);
|
|
156
|
+
const pause = parsePauseState(record.pause, `${filePath}.pause`);
|
|
157
|
+
const abort = parseAbortState(record.abort, `${filePath}.abort`);
|
|
158
|
+
const inflightApprovals = parseInflightApprovals(
|
|
159
|
+
record.inflight_approvals,
|
|
160
|
+
`${filePath}.inflight_approvals`,
|
|
161
|
+
);
|
|
162
|
+
const updatedAt = requireString(record.updated_at, `${filePath}.updated_at`);
|
|
163
|
+
return {
|
|
164
|
+
schema_version: SCHEMA_VERSION_V1,
|
|
165
|
+
session_id: sessionId,
|
|
166
|
+
autonomy_level: autonomyLevel,
|
|
167
|
+
pause,
|
|
168
|
+
abort,
|
|
169
|
+
inflight_approvals: inflightApprovals,
|
|
170
|
+
updated_at: updatedAt,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parsePauseState(value: unknown, filePath: string): RemoteControlPauseState {
|
|
175
|
+
if (value === null || typeof value !== 'object') {
|
|
176
|
+
throw new Error(`Failed to parse remote-control state at ${filePath}: expected pause object.`);
|
|
177
|
+
}
|
|
178
|
+
const record = value as Record<string, unknown>;
|
|
179
|
+
return {
|
|
180
|
+
paused: typeof record.paused === 'boolean' ? record.paused : false,
|
|
181
|
+
paused_at: typeof record.paused_at === 'string' ? record.paused_at : null,
|
|
182
|
+
resumed_at: typeof record.resumed_at === 'string' ? record.resumed_at : null,
|
|
183
|
+
reason: typeof record.reason === 'string' ? record.reason : null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseAbortState(value: unknown, filePath: string): RemoteControlAbortState {
|
|
188
|
+
if (value === null || typeof value !== 'object') {
|
|
189
|
+
throw new Error(`Failed to parse remote-control state at ${filePath}: expected abort object.`);
|
|
190
|
+
}
|
|
191
|
+
const record = value as Record<string, unknown>;
|
|
192
|
+
const cleanupStatus = record.cleanup_status;
|
|
193
|
+
return {
|
|
194
|
+
aborted: typeof record.aborted === 'boolean' ? record.aborted : false,
|
|
195
|
+
aborted_at: typeof record.aborted_at === 'string' ? record.aborted_at : null,
|
|
196
|
+
cleanup_status:
|
|
197
|
+
cleanupStatus === 'clean' || cleanupStatus === 'partial' || cleanupStatus === 'needs_recovery'
|
|
198
|
+
? cleanupStatus
|
|
199
|
+
: null,
|
|
200
|
+
recovery_action: typeof record.recovery_action === 'string' ? record.recovery_action : null,
|
|
201
|
+
reason: typeof record.reason === 'string' ? record.reason : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseInflightApprovals(value: unknown, filePath: string): RemoteControlInflightApproval[] {
|
|
206
|
+
if (!Array.isArray(value)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
return value
|
|
210
|
+
.map((entry, index) => parseInflightApproval(entry, `${filePath}[${index}]`))
|
|
211
|
+
.filter((entry): entry is RemoteControlInflightApproval => entry !== null);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseInflightApproval(
|
|
215
|
+
value: unknown,
|
|
216
|
+
filePath: string,
|
|
217
|
+
): RemoteControlInflightApproval | null {
|
|
218
|
+
if (value === null || typeof value !== 'object') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const record = value as Record<string, unknown>;
|
|
222
|
+
if (
|
|
223
|
+
typeof record.request_id !== 'string' ||
|
|
224
|
+
typeof record.tool_name !== 'string' ||
|
|
225
|
+
typeof record.requested_at !== 'string'
|
|
226
|
+
) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Failed to parse remote-control state at ${filePath}: malformed inflight approval entry.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
request_id: record.request_id,
|
|
233
|
+
tool_name: record.tool_name,
|
|
234
|
+
requested_at: record.requested_at,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function requireString(value: unknown, filePath: string): string {
|
|
239
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Failed to parse remote-control state at ${filePath}: expected a non-empty string.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function requireAutonomyLevel(value: unknown, filePath: string): AgentRuntimeAutonomyLevel {
|
|
248
|
+
if (
|
|
249
|
+
value === AGENT_RUNTIME_AUTONOMY_LEVELS.MANUAL ||
|
|
250
|
+
value === AGENT_RUNTIME_AUTONOMY_LEVELS.ASSISTED ||
|
|
251
|
+
value === AGENT_RUNTIME_AUTONOMY_LEVELS.AUTO
|
|
252
|
+
) {
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Failed to parse remote-control state at ${filePath}: unknown autonomy level "${String(value)}".`,
|
|
257
|
+
);
|
|
258
|
+
}
|