claws-code 0.8.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/.claude/commands/claws-auto.md +90 -0
- package/.claude/commands/claws-bin.md +28 -0
- package/.claude/commands/claws-cleanup.md +28 -0
- package/.claude/commands/claws-do.md +82 -0
- package/.claude/commands/claws-fix.md +40 -0
- package/.claude/commands/claws-goal.md +111 -0
- package/.claude/commands/claws-help.md +54 -0
- package/.claude/commands/claws-plan.md +103 -0
- package/.claude/commands/claws-report.md +29 -0
- package/.claude/commands/claws-status.md +37 -0
- package/.claude/commands/claws-update.md +32 -0
- package/.claude/commands/claws.md +64 -0
- package/.claude/rules/claws-default-behavior.md +76 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.local.json +19 -0
- package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
- package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
- package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
- package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
- package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
- package/CHANGELOG.md +1949 -0
- package/LICENSE +21 -0
- package/README.md +420 -0
- package/bin/cli.js +84 -0
- package/cli.js +223 -0
- package/docs/ARCHITECTURE.md +511 -0
- package/docs/event-protocol.md +588 -0
- package/docs/features.md +562 -0
- package/docs/guide.md +891 -0
- package/docs/index.html +716 -0
- package/docs/protocol.md +323 -0
- package/extension/.vscodeignore +15 -0
- package/extension/CHANGELOG.md +1906 -0
- package/extension/LICENSE +21 -0
- package/extension/README.md +137 -0
- package/extension/docs/features.md +424 -0
- package/extension/docs/protocol.md +197 -0
- package/extension/esbuild.mjs +25 -0
- package/extension/icon.png +0 -0
- package/extension/native/.metadata.json +10 -0
- package/extension/native/node-pty/LICENSE +69 -0
- package/extension/native/node-pty/README.md +165 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
- package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
- package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
- package/extension/native/node-pty/lib/index.js +52 -0
- package/extension/native/node-pty/lib/index.js.map +1 -0
- package/extension/native/node-pty/lib/interfaces.js +7 -0
- package/extension/native/node-pty/lib/interfaces.js.map +1 -0
- package/extension/native/node-pty/lib/shared/conout.js +11 -0
- package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
- package/extension/native/node-pty/lib/terminal.js +190 -0
- package/extension/native/node-pty/lib/terminal.js.map +1 -0
- package/extension/native/node-pty/lib/types.js +7 -0
- package/extension/native/node-pty/lib/types.js.map +1 -0
- package/extension/native/node-pty/lib/unixTerminal.js +346 -0
- package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/utils.js +39 -0
- package/extension/native/node-pty/lib/utils.js.map +1 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
- package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
- package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
- package/extension/native/node-pty/package.json +64 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
- package/extension/package-lock.json +605 -0
- package/extension/package.json +343 -0
- package/extension/scripts/bundle-native.mjs +104 -0
- package/extension/scripts/deploy-dev.mjs +60 -0
- package/extension/src/ansi-strip.ts +52 -0
- package/extension/src/backends/vscode/claws-pty.ts +483 -0
- package/extension/src/backends/vscode/status-bar.ts +99 -0
- package/extension/src/backends/vscode/vscode-backend.ts +282 -0
- package/extension/src/capture-store.ts +125 -0
- package/extension/src/event-log.ts +629 -0
- package/extension/src/event-schemas.ts +478 -0
- package/extension/src/extension.js +492 -0
- package/extension/src/extension.ts +873 -0
- package/extension/src/lifecycle-engine.ts +60 -0
- package/extension/src/lifecycle-rules.ts +171 -0
- package/extension/src/lifecycle-store.ts +506 -0
- package/extension/src/peer-registry.ts +176 -0
- package/extension/src/pipeline-registry.ts +82 -0
- package/extension/src/platform.ts +64 -0
- package/extension/src/protocol.ts +532 -0
- package/extension/src/server-config.ts +98 -0
- package/extension/src/server.ts +2210 -0
- package/extension/src/task-registry.ts +51 -0
- package/extension/src/terminal-backend.ts +211 -0
- package/extension/src/terminal-manager.ts +395 -0
- package/extension/src/topic-registry.ts +70 -0
- package/extension/src/topic-utils.ts +46 -0
- package/extension/src/transport.ts +45 -0
- package/extension/src/uninstall-cleanup.ts +232 -0
- package/extension/src/wave-registry.ts +314 -0
- package/extension/src/websocket-transport.ts +153 -0
- package/extension/tsconfig.json +23 -0
- package/lib/capabilities.js +145 -0
- package/lib/dry-run.js +43 -0
- package/lib/install.js +1018 -0
- package/lib/mcp-setup.js +92 -0
- package/lib/platform.js +240 -0
- package/lib/preflight.js +152 -0
- package/lib/shell-hook.js +343 -0
- package/lib/uninstall.js +162 -0
- package/lib/verify.js +166 -0
- package/mcp_server.js +3529 -0
- package/package.json +48 -0
- package/rules/claws-default-behavior.md +72 -0
- package/scripts/_helpers/atomic-file.mjs +137 -0
- package/scripts/_helpers/fix-repair.js +64 -0
- package/scripts/_helpers/json-safe.mjs +218 -0
- package/scripts/bump-version.sh +84 -0
- package/scripts/codegen/gen-docs.mjs +61 -0
- package/scripts/codegen/gen-json-schema.mjs +62 -0
- package/scripts/codegen/gen-mcp-tools.mjs +358 -0
- package/scripts/codegen/gen-types.mjs +172 -0
- package/scripts/codegen/index.mjs +42 -0
- package/scripts/dev-hooks/check-extension-dirs.js +77 -0
- package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
- package/scripts/dev-hooks/check-stale-main.js +55 -0
- package/scripts/dev-hooks/check-tag-pushed.js +51 -0
- package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
- package/scripts/dev-vsix-install.sh +60 -0
- package/scripts/fix.sh +702 -0
- package/scripts/gen-client-types.mjs +81 -0
- package/scripts/git-hooks/pre-commit +31 -0
- package/scripts/hooks/lifecycle-state.js +61 -0
- package/scripts/hooks/package.json +4 -0
- package/scripts/hooks/post-tool-use-claws.js +292 -0
- package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
- package/scripts/hooks/pre-tool-use-claws.js +206 -0
- package/scripts/hooks/session-start-claws.js +97 -0
- package/scripts/hooks/stop-claws.js +88 -0
- package/scripts/inject-claude-md.js +205 -0
- package/scripts/inject-dev-hooks.js +96 -0
- package/scripts/inject-global-claude-md.js +140 -0
- package/scripts/inject-settings-hooks.js +370 -0
- package/scripts/install.ps1 +146 -0
- package/scripts/install.sh +1729 -0
- package/scripts/monitor-arm-watch.js +155 -0
- package/scripts/rebuild-node-pty.sh +245 -0
- package/scripts/report.sh +232 -0
- package/scripts/shell-hook.fish +164 -0
- package/scripts/shell-hook.ps1 +33 -0
- package/scripts/shell-hook.sh +232 -0
- package/scripts/stream-events.js +399 -0
- package/scripts/terminal-wrapper.sh +36 -0
- package/scripts/test-enforcement.sh +132 -0
- package/scripts/test-install.sh +174 -0
- package/scripts/test-installer-parity.sh +135 -0
- package/scripts/test-template-enforcement.sh +76 -0
- package/scripts/uninstall.sh +143 -0
- package/scripts/update.sh +337 -0
- package/scripts/verify-release.sh +323 -0
- package/scripts/verify-wrapped.sh +194 -0
- package/templates/CLAUDE.global.md +135 -0
- package/templates/CLAUDE.project.md +37 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ─── LifecycleEngine — auto-advance state machine driven by bus events ────────
|
|
2
|
+
// Subscribes (in-process, via direct ClawsServer hookup) to worker state
|
|
3
|
+
// changes. On each event, calls nextAutoPhase(state) from lifecycle-rules.
|
|
4
|
+
// If a transition is recommended AND legal AND gate-checked, calls
|
|
5
|
+
// lifecycleStore.setPhase(next) and emits 'lifecycle.phase-changed' on the
|
|
6
|
+
// bus for orchestrator visibility.
|
|
7
|
+
//
|
|
8
|
+
// Design intent: eliminates orchestrator camping in SPAWN. Phases self-progress
|
|
9
|
+
// as work happens: SPAWN→DEPLOY when all expected workers spawned + monitored,
|
|
10
|
+
// DEPLOY→OBSERVE when first worker progresses, OBSERVE→HARVEST when done
|
|
11
|
+
// (mode-aware), CLEANUP→REFLECT when all terminals closed.
|
|
12
|
+
|
|
13
|
+
import type { LifecycleStore } from './lifecycle-store';
|
|
14
|
+
import { canTransition, canReflect, nextAutoPhase } from './lifecycle-rules';
|
|
15
|
+
|
|
16
|
+
export interface LifecycleEngineDeps {
|
|
17
|
+
store: LifecycleStore;
|
|
18
|
+
emitEvent: (topic: string, payload: unknown) => Promise<void> | void;
|
|
19
|
+
logger?: (msg: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class LifecycleEngine {
|
|
23
|
+
constructor(private readonly deps: LifecycleEngineDeps) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Call after any worker state change (spawn, monitor armed, status update).
|
|
27
|
+
* Loops until no further auto-advance is recommended — handles cascades
|
|
28
|
+
* (e.g. SPAWN→DEPLOY may immediately enable DEPLOY→OBSERVE in edge cases).
|
|
29
|
+
*/
|
|
30
|
+
onWorkerEvent(reason: string): void {
|
|
31
|
+
let safety = 10;
|
|
32
|
+
while (safety-- > 0) {
|
|
33
|
+
const state = this.deps.store.snapshot();
|
|
34
|
+
if (!state) return;
|
|
35
|
+
const next = nextAutoPhase(state);
|
|
36
|
+
if (next === null || next === state.phase) return;
|
|
37
|
+
if (!canTransition(state.phase, next)) {
|
|
38
|
+
this.deps.logger?.(
|
|
39
|
+
`[lifecycle-engine] BLOCKED: nextAutoPhase ${state.phase}→${next} but canTransition=false`,
|
|
40
|
+
);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (next === 'REFLECT' && !canReflect(state).ok) {
|
|
44
|
+
this.deps.logger?.(`[lifecycle-engine] BLOCKED: REFLECT gate not met`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const prev = state.phase;
|
|
48
|
+
this.deps.store.setPhase(next);
|
|
49
|
+
this.deps.logger?.(
|
|
50
|
+
`[lifecycle-engine] auto-advanced ${prev}→${next} (reason: ${reason})`,
|
|
51
|
+
);
|
|
52
|
+
void this.deps.emitEvent('lifecycle.phase-changed', {
|
|
53
|
+
from: prev,
|
|
54
|
+
to: next,
|
|
55
|
+
reason,
|
|
56
|
+
ts: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// ─── Lifecycle rules — pure functions (v0.7.10) ─────────────────────────────
|
|
2
|
+
// Validators + gate predicates + auto-advance decisions. All pure: input
|
|
3
|
+
// LifecycleState → output decision. No I/O, no mutation, no time dependency
|
|
4
|
+
// (callers pass their own clock if needed).
|
|
5
|
+
//
|
|
6
|
+
// This separation lets the engine reason about transitions without touching
|
|
7
|
+
// disk and lets tests cover every gate independently.
|
|
8
|
+
|
|
9
|
+
import type { LifecycleState, Phase, SpawnedWorker, WorkerStatus } from './lifecycle-store';
|
|
10
|
+
|
|
11
|
+
// 10-phase transition graph. SESSION-BOOT and SESSION-END bookend the session;
|
|
12
|
+
// PLAN..REFLECT is the mission cycle, repeatable within a session.
|
|
13
|
+
export const TRANSITIONS: Readonly<Record<Phase, readonly Phase[]>> = {
|
|
14
|
+
'SESSION-BOOT': ['PLAN', 'FAILED'],
|
|
15
|
+
PLAN: ['SPAWN', 'FAILED'],
|
|
16
|
+
SPAWN: ['DEPLOY', 'RECOVER', 'FAILED'],
|
|
17
|
+
DEPLOY: ['OBSERVE', 'RECOVER', 'FAILED'],
|
|
18
|
+
OBSERVE: ['HARVEST', 'RECOVER', 'FAILED'],
|
|
19
|
+
RECOVER: ['DEPLOY', 'OBSERVE', 'FAILED'],
|
|
20
|
+
HARVEST: ['CLEANUP', 'FAILED'],
|
|
21
|
+
CLEANUP: ['REFLECT', 'FAILED'],
|
|
22
|
+
REFLECT: ['PLAN', 'SESSION-END'],
|
|
23
|
+
'SESSION-END': [],
|
|
24
|
+
FAILED: ['CLEANUP', 'SESSION-END'],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TERMINAL_WORKER_STATUSES: ReadonlySet<WorkerStatus> = new Set(['completed', 'failed', 'timeout', 'closed', 'terminated']);
|
|
28
|
+
|
|
29
|
+
/** Pure: is the requested transition legal from the current phase? */
|
|
30
|
+
export function canTransition(from: Phase, to: Phase): boolean {
|
|
31
|
+
if (from === to) return true; // idempotent no-op always allowed
|
|
32
|
+
return TRANSITIONS[from].includes(to);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Why a transition is illegal — for actionable error messages. Returns null if legal.
|
|
37
|
+
* Callers should use canTransition() first; this is for failure-path messaging.
|
|
38
|
+
*/
|
|
39
|
+
export function explainIllegalTransition(from: Phase, to: Phase): string | null {
|
|
40
|
+
if (canTransition(from, to)) return null;
|
|
41
|
+
const allowed = TRANSITIONS[from];
|
|
42
|
+
if (allowed.length === 0) {
|
|
43
|
+
return `phase ${from} is terminal — no transitions allowed`;
|
|
44
|
+
}
|
|
45
|
+
return `cannot go from ${from} to ${to}; allowed: ${allowed.join(', ')}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Spawn-class tools (claws_create / claws_worker / claws_fleet / claws_dispatch_subworker)
|
|
50
|
+
* are only allowed during SPAWN phase, with declared worker_mode + capacity remaining.
|
|
51
|
+
*/
|
|
52
|
+
export function canSpawn(state: LifecycleState | null): { ok: true } | { ok: false; reason: string } {
|
|
53
|
+
if (!state) return { ok: false, reason: 'no lifecycle state — call lifecycle.plan first' };
|
|
54
|
+
if (state.phase !== 'SPAWN') {
|
|
55
|
+
return { ok: false, reason: `spawn allowed only in SPAWN phase, currently ${state.phase}` };
|
|
56
|
+
}
|
|
57
|
+
if (state.spawned_workers.length >= state.expected_workers) {
|
|
58
|
+
return { ok: false, reason: `expected_workers=${state.expected_workers} already spawned` };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** CLEANUP gate: every spawned worker must have reached terminal status. */
|
|
64
|
+
export function canCleanup(state: LifecycleState | null): { ok: true } | { ok: false; reason: string } {
|
|
65
|
+
if (!state) return { ok: false, reason: 'no lifecycle state' };
|
|
66
|
+
const incomplete = state.spawned_workers.filter(w => !TERMINAL_WORKER_STATUSES.has(w.status));
|
|
67
|
+
if (incomplete.length > 0) {
|
|
68
|
+
return { ok: false, reason: `${incomplete.length} worker(s) not at terminal status: ` + incomplete.map(w => `${w.id}(${w.status})`).join(', ') };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** REFLECT gate: every spawned terminal must be closed. */
|
|
74
|
+
export function canReflect(state: LifecycleState | null): { ok: true } | { ok: false; reason: string } {
|
|
75
|
+
if (!state) return { ok: false, reason: 'no lifecycle state' };
|
|
76
|
+
const stillOpen = state.spawned_workers.filter(w => w.status !== 'closed');
|
|
77
|
+
if (stillOpen.length > 0) {
|
|
78
|
+
return { ok: false, reason: `${stillOpen.length} terminal(s) still open: ` + stillOpen.map(w => w.id).join(', ') };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** SESSION-END gate: phase must be REFLECT or FAILED + zero open terminals. */
|
|
84
|
+
export function canEndSession(state: LifecycleState | null): { ok: true } | { ok: false; reason: string } {
|
|
85
|
+
if (!state) return { ok: false, reason: 'no lifecycle state' };
|
|
86
|
+
if (state.phase !== 'REFLECT' && state.phase !== 'FAILED') {
|
|
87
|
+
return { ok: false, reason: `session-end requires phase REFLECT or FAILED, got ${state.phase}` };
|
|
88
|
+
}
|
|
89
|
+
const stillOpen = state.spawned_workers.filter(w => w.status !== 'closed');
|
|
90
|
+
if (stillOpen.length > 0) {
|
|
91
|
+
return { ok: false, reason: `${stillOpen.length} terminal(s) still open` };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True iff every spawned worker has a registered monitor. */
|
|
97
|
+
export function allWorkersHaveMonitors(state: LifecycleState | null): boolean {
|
|
98
|
+
if (!state) return false;
|
|
99
|
+
const monitoredIds = new Set(state.monitors.map(m => m.terminal_id));
|
|
100
|
+
return state.spawned_workers.every(w => monitoredIds.has(w.id));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Terminal IDs of spawned workers without monitors — for diagnostics. */
|
|
104
|
+
export function workersWithoutMonitors(state: LifecycleState | null): string[] {
|
|
105
|
+
if (!state) return [];
|
|
106
|
+
const monitoredIds = new Set(state.monitors.map(m => m.terminal_id));
|
|
107
|
+
return state.spawned_workers.filter(w => !monitoredIds.has(w.id)).map(w => w.id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Auto-advance decision. Given current state, returns the phase the engine SHOULD
|
|
112
|
+
* transition to next, or null if no auto-advance applies. Pure function — engine
|
|
113
|
+
* decides whether to act on the recommendation.
|
|
114
|
+
*
|
|
115
|
+
* Rules (mode-specific):
|
|
116
|
+
* - SPAWN → DEPLOY when all expected_workers spawned + all have monitors
|
|
117
|
+
* - DEPLOY → OBSERVE when at least one worker has progressed (status !== spawned)
|
|
118
|
+
* - OBSERVE → HARVEST when all workers reach terminal status
|
|
119
|
+
* - single: 1 terminal-status worker
|
|
120
|
+
* - fleet: all expected_workers at terminal status
|
|
121
|
+
* - army: driven by claws_wave_complete (engine doesn't auto-advance OBSERVE→HARVEST for army)
|
|
122
|
+
* - CLEANUP → REFLECT (auto when all closed; gate enforces)
|
|
123
|
+
*/
|
|
124
|
+
export function nextAutoPhase(state: LifecycleState | null): Phase | null {
|
|
125
|
+
if (!state) return null;
|
|
126
|
+
switch (state.phase) {
|
|
127
|
+
case 'SPAWN':
|
|
128
|
+
if (state.spawned_workers.length === state.expected_workers && allWorkersHaveMonitors(state)) {
|
|
129
|
+
return 'DEPLOY';
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
case 'DEPLOY':
|
|
133
|
+
if (state.spawned_workers.some(w => w.status !== 'spawned')) {
|
|
134
|
+
return 'OBSERVE';
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
case 'OBSERVE': {
|
|
138
|
+
if (state.worker_mode === 'army') return null; // army uses claws_wave_complete
|
|
139
|
+
const terminalStatuses = state.spawned_workers.filter(w => TERMINAL_WORKER_STATUSES.has(w.status));
|
|
140
|
+
if (state.worker_mode === 'single' && terminalStatuses.length >= 1) return 'HARVEST';
|
|
141
|
+
if (state.worker_mode === 'fleet' && terminalStatuses.length >= state.expected_workers) return 'HARVEST';
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
case 'HARVEST':
|
|
145
|
+
// BUG-A fix: HARVEST→CLEANUP auto when all workers reached terminal status.
|
|
146
|
+
// (canCleanup gate enforces it; engine just needed the explicit transition.)
|
|
147
|
+
if (canCleanup(state).ok) return 'CLEANUP';
|
|
148
|
+
return null;
|
|
149
|
+
case 'CLEANUP':
|
|
150
|
+
if (canReflect(state).ok) return 'REFLECT';
|
|
151
|
+
return null;
|
|
152
|
+
default:
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Helper for tests: terminal worker statuses set. */
|
|
158
|
+
export function isTerminalStatus(status: WorkerStatus): boolean {
|
|
159
|
+
return TERMINAL_WORKER_STATUSES.has(status);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Helper for tests: count workers by status. */
|
|
163
|
+
export function workerStatusCounts(state: LifecycleState | null): Record<WorkerStatus, number> {
|
|
164
|
+
const counts: Record<WorkerStatus, number> = { spawned: 0, completed: 0, failed: 0, timeout: 0, closed: 0, terminated: 0 };
|
|
165
|
+
if (!state) return counts;
|
|
166
|
+
for (const w of state.spawned_workers) counts[w.status]++;
|
|
167
|
+
return counts;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Re-export for ergonomic imports
|
|
171
|
+
export type { SpawnedWorker };
|