@ginkoai/cli 1.6.2 → 1.7.1
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/dist/commands/agent/agent-client.d.ts +150 -0
- package/dist/commands/agent/agent-client.d.ts.map +1 -0
- package/dist/commands/agent/agent-client.js +170 -0
- package/dist/commands/agent/agent-client.js.map +1 -0
- package/dist/commands/agent/index.d.ts +22 -0
- package/dist/commands/agent/index.d.ts.map +1 -0
- package/dist/commands/agent/index.js +121 -0
- package/dist/commands/agent/index.js.map +1 -0
- package/dist/commands/agent/list.d.ts +22 -0
- package/dist/commands/agent/list.d.ts.map +1 -0
- package/dist/commands/agent/list.js +119 -0
- package/dist/commands/agent/list.js.map +1 -0
- package/dist/commands/agent/register.d.ts +21 -0
- package/dist/commands/agent/register.d.ts.map +1 -0
- package/dist/commands/agent/register.js +97 -0
- package/dist/commands/agent/register.js.map +1 -0
- package/dist/commands/agent/status.d.ts +19 -0
- package/dist/commands/agent/status.d.ts.map +1 -0
- package/dist/commands/agent/status.js +271 -0
- package/dist/commands/agent/status.js.map +1 -0
- package/dist/commands/agent/work.d.ts +22 -0
- package/dist/commands/agent/work.d.ts.map +1 -0
- package/dist/commands/agent/work.js +459 -0
- package/dist/commands/agent/work.js.map +1 -0
- package/dist/commands/checkpoint/create.d.ts +27 -0
- package/dist/commands/checkpoint/create.d.ts.map +1 -0
- package/dist/commands/checkpoint/create.js +82 -0
- package/dist/commands/checkpoint/create.js.map +1 -0
- package/dist/commands/checkpoint/index.d.ts +23 -0
- package/dist/commands/checkpoint/index.d.ts.map +1 -0
- package/dist/commands/checkpoint/index.js +91 -0
- package/dist/commands/checkpoint/index.js.map +1 -0
- package/dist/commands/checkpoint/list.d.ts +27 -0
- package/dist/commands/checkpoint/list.d.ts.map +1 -0
- package/dist/commands/checkpoint/list.js +115 -0
- package/dist/commands/checkpoint/list.js.map +1 -0
- package/dist/commands/checkpoint/show.d.ts +23 -0
- package/dist/commands/checkpoint/show.d.ts.map +1 -0
- package/dist/commands/checkpoint/show.js +102 -0
- package/dist/commands/checkpoint/show.js.map +1 -0
- package/dist/commands/dlq.d.ts +24 -0
- package/dist/commands/dlq.d.ts.map +1 -0
- package/dist/commands/dlq.js +172 -0
- package/dist/commands/dlq.js.map +1 -0
- package/dist/commands/escalation/create.d.ts +22 -0
- package/dist/commands/escalation/create.d.ts.map +1 -0
- package/dist/commands/escalation/create.js +122 -0
- package/dist/commands/escalation/create.js.map +1 -0
- package/dist/commands/escalation/escalation-client.d.ts +101 -0
- package/dist/commands/escalation/escalation-client.d.ts.map +1 -0
- package/dist/commands/escalation/escalation-client.js +129 -0
- package/dist/commands/escalation/escalation-client.js.map +1 -0
- package/dist/commands/escalation/index.d.ts +22 -0
- package/dist/commands/escalation/index.d.ts.map +1 -0
- package/dist/commands/escalation/index.js +94 -0
- package/dist/commands/escalation/index.js.map +1 -0
- package/dist/commands/escalation/list.d.ts +24 -0
- package/dist/commands/escalation/list.d.ts.map +1 -0
- package/dist/commands/escalation/list.js +170 -0
- package/dist/commands/escalation/list.js.map +1 -0
- package/dist/commands/escalation/resolve.d.ts +20 -0
- package/dist/commands/escalation/resolve.d.ts.map +1 -0
- package/dist/commands/escalation/resolve.js +102 -0
- package/dist/commands/escalation/resolve.js.map +1 -0
- package/dist/commands/graph/api-client.d.ts +21 -1
- package/dist/commands/graph/api-client.d.ts.map +1 -1
- package/dist/commands/graph/api-client.js +23 -0
- package/dist/commands/graph/api-client.js.map +1 -1
- package/dist/commands/handoff.d.ts.map +1 -1
- package/dist/commands/handoff.js +9 -1
- package/dist/commands/handoff.js.map +1 -1
- package/dist/commands/log.d.ts +3 -0
- package/dist/commands/log.d.ts.map +1 -1
- package/dist/commands/log.js +73 -14
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/notifications/history.d.ts +21 -0
- package/dist/commands/notifications/history.d.ts.map +1 -0
- package/dist/commands/notifications/history.js +160 -0
- package/dist/commands/notifications/history.js.map +1 -0
- package/dist/commands/notifications/index.d.ts +22 -0
- package/dist/commands/notifications/index.d.ts.map +1 -0
- package/dist/commands/notifications/index.js +87 -0
- package/dist/commands/notifications/index.js.map +1 -0
- package/dist/commands/notifications/list.d.ts +19 -0
- package/dist/commands/notifications/list.d.ts.map +1 -0
- package/dist/commands/notifications/list.js +132 -0
- package/dist/commands/notifications/list.js.map +1 -0
- package/dist/commands/notifications/test.d.ts +19 -0
- package/dist/commands/notifications/test.d.ts.map +1 -0
- package/dist/commands/notifications/test.js +217 -0
- package/dist/commands/notifications/test.js.map +1 -0
- package/dist/commands/orchestrate.d.ts +25 -0
- package/dist/commands/orchestrate.d.ts.map +1 -0
- package/dist/commands/orchestrate.js +858 -0
- package/dist/commands/orchestrate.js.map +1 -0
- package/dist/commands/sprint/deps.d.ts +29 -0
- package/dist/commands/sprint/deps.d.ts.map +1 -0
- package/dist/commands/sprint/deps.js +269 -0
- package/dist/commands/sprint/deps.js.map +1 -0
- package/dist/commands/sprint/index.d.ts +10 -5
- package/dist/commands/sprint/index.d.ts.map +1 -1
- package/dist/commands/sprint/index.js +26 -5
- package/dist/commands/sprint/index.js.map +1 -1
- package/dist/commands/start/index.d.ts.map +1 -1
- package/dist/commands/start/index.js +6 -0
- package/dist/commands/start/index.js.map +1 -1
- package/dist/commands/start/start-reflection.d.ts.map +1 -1
- package/dist/commands/start/start-reflection.js +8 -0
- package/dist/commands/start/start-reflection.js.map +1 -1
- package/dist/commands/verify.d.ts +17 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +232 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/core/session-log-manager.d.ts +1 -1
- package/dist/core/session-log-manager.d.ts.map +1 -1
- package/dist/index.js +78 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/__tests__/task-timeout.test.d.ts +12 -0
- package/dist/lib/__tests__/task-timeout.test.d.ts.map +1 -0
- package/dist/lib/__tests__/task-timeout.test.js +278 -0
- package/dist/lib/__tests__/task-timeout.test.js.map +1 -0
- package/dist/lib/agent-heartbeat.d.ts +68 -0
- package/dist/lib/agent-heartbeat.d.ts.map +1 -0
- package/dist/lib/agent-heartbeat.js +117 -0
- package/dist/lib/agent-heartbeat.js.map +1 -0
- package/dist/lib/checkpoint.d.ts +85 -0
- package/dist/lib/checkpoint.d.ts.map +1 -0
- package/dist/lib/checkpoint.js +323 -0
- package/dist/lib/checkpoint.js.map +1 -0
- package/dist/lib/context-metrics.d.ts +230 -0
- package/dist/lib/context-metrics.d.ts.map +1 -0
- package/dist/lib/context-metrics.js +372 -0
- package/dist/lib/context-metrics.js.map +1 -0
- package/dist/lib/dead-letter-queue.d.ts +108 -0
- package/dist/lib/dead-letter-queue.d.ts.map +1 -0
- package/dist/lib/dead-letter-queue.js +378 -0
- package/dist/lib/dead-letter-queue.js.map +1 -0
- package/dist/lib/event-logger.d.ts +9 -1
- package/dist/lib/event-logger.d.ts.map +1 -1
- package/dist/lib/event-logger.js +45 -3
- package/dist/lib/event-logger.js.map +1 -1
- package/dist/lib/event-queue.d.ts.map +1 -1
- package/dist/lib/event-queue.js +13 -2
- package/dist/lib/event-queue.js.map +1 -1
- package/dist/lib/examples/timeout-demo.d.ts +13 -0
- package/dist/lib/examples/timeout-demo.d.ts.map +1 -0
- package/dist/lib/examples/timeout-demo.js +102 -0
- package/dist/lib/examples/timeout-demo.js.map +1 -0
- package/dist/lib/examples/timeout-integration-example.d.ts +17 -0
- package/dist/lib/examples/timeout-integration-example.d.ts.map +1 -0
- package/dist/lib/examples/timeout-integration-example.js +223 -0
- package/dist/lib/examples/timeout-integration-example.js.map +1 -0
- package/dist/lib/notification-hooks.d.ts +103 -0
- package/dist/lib/notification-hooks.d.ts.map +1 -0
- package/dist/lib/notification-hooks.js +223 -0
- package/dist/lib/notification-hooks.js.map +1 -0
- package/dist/lib/notifications/discord.d.ts +20 -0
- package/dist/lib/notifications/discord.d.ts.map +1 -0
- package/dist/lib/notifications/discord.js +140 -0
- package/dist/lib/notifications/discord.js.map +1 -0
- package/dist/lib/notifications/index.d.ts +66 -0
- package/dist/lib/notifications/index.d.ts.map +1 -0
- package/dist/lib/notifications/index.js +120 -0
- package/dist/lib/notifications/index.js.map +1 -0
- package/dist/lib/notifications/slack.d.ts +20 -0
- package/dist/lib/notifications/slack.d.ts.map +1 -0
- package/dist/lib/notifications/slack.js +186 -0
- package/dist/lib/notifications/slack.js.map +1 -0
- package/dist/lib/notifications/teams.d.ts +20 -0
- package/dist/lib/notifications/teams.d.ts.map +1 -0
- package/dist/lib/notifications/teams.js +146 -0
- package/dist/lib/notifications/teams.js.map +1 -0
- package/dist/lib/notifications/webhook.d.ts +23 -0
- package/dist/lib/notifications/webhook.d.ts.map +1 -0
- package/dist/lib/notifications/webhook.js +65 -0
- package/dist/lib/notifications/webhook.js.map +1 -0
- package/dist/lib/orchestrator-state.d.ts +194 -0
- package/dist/lib/orchestrator-state.d.ts.map +1 -0
- package/dist/lib/orchestrator-state.js +332 -0
- package/dist/lib/orchestrator-state.js.map +1 -0
- package/dist/lib/realtime-cursor.d.ts +107 -0
- package/dist/lib/realtime-cursor.d.ts.map +1 -0
- package/dist/lib/realtime-cursor.js +260 -0
- package/dist/lib/realtime-cursor.js.map +1 -0
- package/dist/lib/rollback.d.ts +86 -0
- package/dist/lib/rollback.d.ts.map +1 -0
- package/dist/lib/rollback.js +405 -0
- package/dist/lib/rollback.js.map +1 -0
- package/dist/lib/sprint-loader.d.ts +39 -2
- package/dist/lib/sprint-loader.d.ts.map +1 -1
- package/dist/lib/sprint-loader.js +269 -5
- package/dist/lib/sprint-loader.js.map +1 -1
- package/dist/lib/stale-agent-detector.d.ts +102 -0
- package/dist/lib/stale-agent-detector.d.ts.map +1 -0
- package/dist/lib/stale-agent-detector.js +156 -0
- package/dist/lib/stale-agent-detector.js.map +1 -0
- package/dist/lib/task-dependencies.d.ts +143 -0
- package/dist/lib/task-dependencies.d.ts.map +1 -0
- package/dist/lib/task-dependencies.js +357 -0
- package/dist/lib/task-dependencies.js.map +1 -0
- package/dist/lib/task-timeout.d.ts +153 -0
- package/dist/lib/task-timeout.d.ts.map +1 -0
- package/dist/lib/task-timeout.js +505 -0
- package/dist/lib/task-timeout.js.map +1 -0
- package/dist/lib/verification/build-check.d.ts +55 -0
- package/dist/lib/verification/build-check.d.ts.map +1 -0
- package/dist/lib/verification/build-check.js +111 -0
- package/dist/lib/verification/build-check.js.map +1 -0
- package/dist/lib/verification/index.d.ts +19 -0
- package/dist/lib/verification/index.d.ts.map +1 -0
- package/dist/lib/verification/index.js +17 -0
- package/dist/lib/verification/index.js.map +1 -0
- package/dist/lib/verification/lint-check.d.ts +34 -0
- package/dist/lib/verification/lint-check.d.ts.map +1 -0
- package/dist/lib/verification/lint-check.js +215 -0
- package/dist/lib/verification/lint-check.js.map +1 -0
- package/dist/lib/verification/test-runner.d.ts +50 -0
- package/dist/lib/verification/test-runner.d.ts.map +1 -0
- package/dist/lib/verification/test-runner.js +225 -0
- package/dist/lib/verification/test-runner.js.map +1 -0
- package/dist/utils/command-helpers.d.ts.map +1 -1
- package/dist/utils/command-helpers.js +7 -0
- package/dist/utils/command-helpers.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileType: command
|
|
3
|
+
* @status: current
|
|
4
|
+
* @updated: 2025-12-07
|
|
5
|
+
* @tags: [cli, orchestrate, multi-agent, supervisor, epic-004, sprint-4, task-10]
|
|
6
|
+
* @related: [agent/index.ts, agent/work.ts, ../lib/task-dependencies.ts, graph/api-client.ts, ../lib/orchestrator-state.ts]
|
|
7
|
+
* @priority: high
|
|
8
|
+
* @complexity: high
|
|
9
|
+
* @dependencies: [commander, chalk, ora, fs-extra]
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Orchestrate Command (EPIC-004 Sprint 4 TASK-7, TASK-10)
|
|
13
|
+
*
|
|
14
|
+
* Run as supervisor agent to coordinate multi-agent task execution.
|
|
15
|
+
*
|
|
16
|
+
* The orchestrator:
|
|
17
|
+
* 1. Registers as an orchestrator agent (or resumes from checkpoint)
|
|
18
|
+
* 2. Loads sprint tasks with dependencies
|
|
19
|
+
* 3. Computes execution waves (topological ordering)
|
|
20
|
+
* 4. Monitors available worker agents
|
|
21
|
+
* 5. Assigns tasks to workers based on capabilities
|
|
22
|
+
* 6. Reacts to task completion events
|
|
23
|
+
* 7. Handles blockers and reassignment
|
|
24
|
+
* 8. Saves checkpoint on exit for seamless respawn (TASK-10)
|
|
25
|
+
*
|
|
26
|
+
* Exit Conditions (TASK-10):
|
|
27
|
+
* - All tasks complete → success exit (code 0), checkpoint deleted
|
|
28
|
+
* - Context pressure > 80% → checkpoint + respawn (code 75)
|
|
29
|
+
* - No progress for 10 cycles → escalate + pause (code 1)
|
|
30
|
+
* - Human interrupt (SIGINT) → graceful shutdown (code 0)
|
|
31
|
+
* - Max runtime exceeded → checkpoint + respawn (code 75)
|
|
32
|
+
*
|
|
33
|
+
* Resume Flow (TASK-10):
|
|
34
|
+
* - Use --resume to continue from last checkpoint
|
|
35
|
+
* - Restores completed tasks, in-progress assignments, context metrics
|
|
36
|
+
* - New instance seamlessly continues orchestration
|
|
37
|
+
*/
|
|
38
|
+
import chalk from 'chalk';
|
|
39
|
+
import ora from 'ora';
|
|
40
|
+
import { AgentClient } from './agent/agent-client.js';
|
|
41
|
+
import { GraphApiClient } from './graph/api-client.js';
|
|
42
|
+
import { requireGinkoRoot } from '../utils/ginko-root.js';
|
|
43
|
+
import { loadGraphConfig } from './graph/config.js';
|
|
44
|
+
import { loadSprintChecklist } from '../lib/sprint-loader.js';
|
|
45
|
+
import { getExecutionOrder, validateDependencies, getDependencyStats, } from '../lib/task-dependencies.js';
|
|
46
|
+
import { startHeartbeat, shutdownHeartbeat } from '../lib/agent-heartbeat.js';
|
|
47
|
+
import { getContextMonitor, resetContextMonitor, getPressureColor, } from '../lib/context-metrics.js';
|
|
48
|
+
import { OrchestratorStateManager, EXIT_CODE_SUCCESS, EXIT_CODE_ERROR, EXIT_CODE_RESPAWN, getExitMessage, persistStateToGraph, recoverStateFromGraph, reconcileTaskStatuses, } from '../lib/orchestrator-state.js';
|
|
49
|
+
// ============================================================
|
|
50
|
+
// Constants
|
|
51
|
+
// ============================================================
|
|
52
|
+
const MAX_CYCLES_WITHOUT_PROGRESS = 10;
|
|
53
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
54
|
+
const DEFAULT_MAX_RUNTIME_MINUTES = 60;
|
|
55
|
+
// EXIT_CODE_RESPAWN imported from orchestrator-state.ts
|
|
56
|
+
// ============================================================
|
|
57
|
+
// Main Command
|
|
58
|
+
// ============================================================
|
|
59
|
+
/**
|
|
60
|
+
* Run orchestrator agent
|
|
61
|
+
*/
|
|
62
|
+
export async function orchestrateCommand(options = {}) {
|
|
63
|
+
let state = null;
|
|
64
|
+
let spinner = null;
|
|
65
|
+
let isShuttingDown = false;
|
|
66
|
+
let stateManager = null;
|
|
67
|
+
let resumedFromCheckpoint = false;
|
|
68
|
+
try {
|
|
69
|
+
// ============================================================
|
|
70
|
+
// PHASE 1: Initialization
|
|
71
|
+
// ============================================================
|
|
72
|
+
spinner = ora('Initializing orchestrator...').start();
|
|
73
|
+
const projectRoot = await requireGinkoRoot();
|
|
74
|
+
const graphConfig = await loadGraphConfig();
|
|
75
|
+
if (!graphConfig?.graphId) {
|
|
76
|
+
spinner.fail(chalk.red('No graph configured'));
|
|
77
|
+
console.error(chalk.red(' Run `ginko graph init` to initialize the graph first.'));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
// ============================================================
|
|
81
|
+
// PHASE 2: Load Sprint & Tasks
|
|
82
|
+
// ============================================================
|
|
83
|
+
spinner.text = 'Loading sprint tasks...';
|
|
84
|
+
const sprint = await loadSprintChecklist(projectRoot);
|
|
85
|
+
if (!sprint) {
|
|
86
|
+
spinner.fail(chalk.red('No active sprint found'));
|
|
87
|
+
console.error(chalk.red(' Create a sprint file in docs/sprints/ first.'));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// TASK-10: Initialize state manager
|
|
91
|
+
stateManager = new OrchestratorStateManager(projectRoot, graphConfig.graphId);
|
|
92
|
+
// TASK-8: Try to recover from graph first (cross-machine recovery)
|
|
93
|
+
const client = new GraphApiClient();
|
|
94
|
+
let graphCheckpoint = null;
|
|
95
|
+
// TASK-10: Check for existing checkpoint
|
|
96
|
+
if (options.resume) {
|
|
97
|
+
spinner.text = 'Checking for checkpoint...';
|
|
98
|
+
// TASK-8: Try graph recovery first
|
|
99
|
+
try {
|
|
100
|
+
const epicId = options.epic || sprint.file || 'unknown';
|
|
101
|
+
graphCheckpoint = await recoverStateFromGraph(graphConfig.graphId, epicId, client);
|
|
102
|
+
if (graphCheckpoint) {
|
|
103
|
+
spinner.succeed(chalk.green('Recovered state from graph (cross-machine recovery)'));
|
|
104
|
+
console.log(chalk.dim(` Orchestrator: ${graphCheckpoint.orchestratorName}`));
|
|
105
|
+
console.log(chalk.dim(` Last persisted: ${graphCheckpoint.persistedAt || graphCheckpoint.savedAt}`));
|
|
106
|
+
console.log(chalk.dim(` Completed: ${graphCheckpoint.completedTasks.length} tasks`));
|
|
107
|
+
console.log(chalk.dim(` In progress: ${Object.keys(graphCheckpoint.inProgressTasks).length} tasks`));
|
|
108
|
+
resumedFromCheckpoint = true;
|
|
109
|
+
spinner = ora('Resuming orchestration...').start();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
// Graph recovery not available, fall back to local checkpoint
|
|
114
|
+
console.log(chalk.dim(' Graph recovery unavailable, checking local checkpoint...'));
|
|
115
|
+
}
|
|
116
|
+
// Fall back to local checkpoint if graph recovery failed
|
|
117
|
+
if (!graphCheckpoint) {
|
|
118
|
+
const checkpoint = await stateManager.loadCheckpoint();
|
|
119
|
+
if (checkpoint) {
|
|
120
|
+
graphCheckpoint = checkpoint;
|
|
121
|
+
spinner.succeed(chalk.green('Found checkpoint from previous session (local recovery)'));
|
|
122
|
+
console.log(chalk.dim(` Saved at: ${checkpoint.savedAt}`));
|
|
123
|
+
console.log(chalk.dim(` Completed: ${checkpoint.completedTasks.length} tasks`));
|
|
124
|
+
console.log(chalk.dim(` In progress: ${Object.keys(checkpoint.inProgressTasks).length} tasks`));
|
|
125
|
+
if (checkpoint.exitReason) {
|
|
126
|
+
console.log(chalk.dim(` Exit reason: ${getExitMessage(checkpoint.exitReason)}`));
|
|
127
|
+
}
|
|
128
|
+
resumedFromCheckpoint = true;
|
|
129
|
+
spinner = ora('Resuming orchestration...').start();
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
spinner.warn(chalk.yellow('No checkpoint found - starting fresh'));
|
|
133
|
+
console.log(chalk.dim(' Use --resume only after a previous session saved a checkpoint'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Check if checkpoint exists and warn user
|
|
139
|
+
const hasCheckpoint = await stateManager.hasCheckpoint();
|
|
140
|
+
if (hasCheckpoint) {
|
|
141
|
+
spinner.info(chalk.cyan('Previous checkpoint found'));
|
|
142
|
+
console.log(chalk.dim(' Use --resume to continue from last session'));
|
|
143
|
+
console.log(chalk.dim(' Starting fresh will overwrite the checkpoint'));
|
|
144
|
+
console.log('');
|
|
145
|
+
spinner = ora('Starting fresh orchestration...').start();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Convert sprint tasks to orchestrator tasks
|
|
149
|
+
const tasks = convertSprintTasks(sprint);
|
|
150
|
+
if (tasks.length === 0) {
|
|
151
|
+
spinner.fail(chalk.red('No tasks found in sprint'));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
// Validate dependencies
|
|
155
|
+
const depTasks = tasks.map(t => ({
|
|
156
|
+
id: t.id,
|
|
157
|
+
dependsOn: t.dependsOn,
|
|
158
|
+
status: mapStatusForDeps(t.status),
|
|
159
|
+
}));
|
|
160
|
+
const errors = validateDependencies(depTasks);
|
|
161
|
+
const hasBlockingErrors = errors.some(e => e.type === 'circular' || e.type === 'self_reference');
|
|
162
|
+
if (errors.length > 0) {
|
|
163
|
+
spinner.warn(chalk.yellow('Dependency warnings:'));
|
|
164
|
+
for (const error of errors) {
|
|
165
|
+
console.log(chalk.yellow(` ⚠️ ${error.details}`));
|
|
166
|
+
}
|
|
167
|
+
// For self-references and circular deps, remove the problematic deps to continue
|
|
168
|
+
if (hasBlockingErrors) {
|
|
169
|
+
console.log(chalk.dim(' Removing problematic dependencies to continue...'));
|
|
170
|
+
for (const task of tasks) {
|
|
171
|
+
// Remove self-references
|
|
172
|
+
task.dependsOn = task.dependsOn.filter(d => d !== task.id);
|
|
173
|
+
// Update depTasks too
|
|
174
|
+
const depTask = depTasks.find(t => t.id === task.id);
|
|
175
|
+
if (depTask) {
|
|
176
|
+
depTask.dependsOn = task.dependsOn;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Compute execution waves
|
|
182
|
+
let waves;
|
|
183
|
+
try {
|
|
184
|
+
waves = getExecutionOrder(depTasks);
|
|
185
|
+
}
|
|
186
|
+
catch (waveError) {
|
|
187
|
+
// If still failing, fallback to treating all tasks as wave 1 (no deps)
|
|
188
|
+
spinner.warn(chalk.yellow('Cannot compute optimal wave order, using flat execution'));
|
|
189
|
+
waves = [{
|
|
190
|
+
wave: 1,
|
|
191
|
+
tasks: depTasks,
|
|
192
|
+
}];
|
|
193
|
+
}
|
|
194
|
+
const stats = getDependencyStats(depTasks);
|
|
195
|
+
spinner.succeed(chalk.green(`Loaded ${tasks.length} tasks in ${waves.length} waves`));
|
|
196
|
+
// ============================================================
|
|
197
|
+
// PHASE 3: Display Initial Status (before registration for dry-run)
|
|
198
|
+
// ============================================================
|
|
199
|
+
console.log('');
|
|
200
|
+
displayOrchestrationPlan(tasks, waves, stats, options.verbose);
|
|
201
|
+
if (options.dryRun) {
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log(chalk.yellow('🔍 Dry run mode - no tasks will be assigned'));
|
|
204
|
+
console.log(chalk.dim(' Remove --dry-run to start orchestration'));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// ============================================================
|
|
208
|
+
// PHASE 4: Register Orchestrator Agent (or restore from checkpoint)
|
|
209
|
+
// ============================================================
|
|
210
|
+
// TASK-10 & TASK-8: Check if resuming from checkpoint
|
|
211
|
+
let checkpoint = graphCheckpoint;
|
|
212
|
+
if (checkpoint && resumedFromCheckpoint) {
|
|
213
|
+
// TASK-8: Reconcile task statuses before restoration
|
|
214
|
+
spinner.text = 'Reconciling task statuses...';
|
|
215
|
+
const actualTasks = tasks.map(t => ({
|
|
216
|
+
id: t.id,
|
|
217
|
+
status: t.status === 'complete' ? 'complete' : t.status,
|
|
218
|
+
}));
|
|
219
|
+
checkpoint = await reconcileTaskStatuses(checkpoint, actualTasks, client);
|
|
220
|
+
// TASK-10: Restore state from checkpoint
|
|
221
|
+
spinner.text = 'Restoring state from checkpoint...';
|
|
222
|
+
// Restore context monitor with previous metrics
|
|
223
|
+
resetContextMonitor();
|
|
224
|
+
const contextMonitor = getContextMonitor({
|
|
225
|
+
model: checkpoint.contextMetrics.model || 'claude-opus-4-5-20251101',
|
|
226
|
+
});
|
|
227
|
+
// Restore token count from checkpoint
|
|
228
|
+
contextMonitor.addTokens(checkpoint.contextMetrics.estimatedTokens);
|
|
229
|
+
// Restore state
|
|
230
|
+
const restored = stateManager.restoreFromCheckpoint(checkpoint);
|
|
231
|
+
state = {
|
|
232
|
+
orchestratorId: checkpoint.orchestratorId,
|
|
233
|
+
orchestratorName: checkpoint.orchestratorName,
|
|
234
|
+
graphId: checkpoint.graphId,
|
|
235
|
+
sprintId: checkpoint.sprintId,
|
|
236
|
+
startedAt: restored.startedAt,
|
|
237
|
+
lastProgressAt: new Date(), // Reset progress timer for new session
|
|
238
|
+
cyclesWithoutProgress: 0, // Reset cycle counter
|
|
239
|
+
completedTasks: restored.completedTasks,
|
|
240
|
+
inProgressTasks: restored.inProgressTasks,
|
|
241
|
+
blockedTasks: restored.blockedTasks,
|
|
242
|
+
assignmentHistory: restored.assignmentHistory,
|
|
243
|
+
contextMonitor,
|
|
244
|
+
recoveredFromCheckpointId: checkpoint.orchestratorId, // Track recovery source
|
|
245
|
+
};
|
|
246
|
+
// Update task statuses from restored state
|
|
247
|
+
for (const task of tasks) {
|
|
248
|
+
if (state.completedTasks.has(task.id)) {
|
|
249
|
+
task.status = 'complete';
|
|
250
|
+
}
|
|
251
|
+
else if (state.inProgressTasks.has(task.id)) {
|
|
252
|
+
task.status = 'assigned';
|
|
253
|
+
task.assignedTo = state.inProgressTasks.get(task.id);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
spinner.succeed(chalk.green(`Resumed as ${chalk.bold(state.orchestratorName)}`));
|
|
257
|
+
console.log(chalk.dim(` Agent ID: ${state.orchestratorId}`));
|
|
258
|
+
console.log(chalk.dim(` Restored: ${state.completedTasks.size} completed, ${state.inProgressTasks.size} in progress`));
|
|
259
|
+
if (checkpoint.persistedAt) {
|
|
260
|
+
console.log(chalk.dim(` Recovered from: ${checkpoint.recoveredFrom || 'graph state'}`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Fresh start - register new orchestrator
|
|
265
|
+
spinner = ora('Registering orchestrator agent...').start();
|
|
266
|
+
const orchestratorName = `orchestrator-${Date.now()}`;
|
|
267
|
+
let registerResponse;
|
|
268
|
+
try {
|
|
269
|
+
registerResponse = await AgentClient.register({
|
|
270
|
+
name: orchestratorName,
|
|
271
|
+
capabilities: ['task-assignment', 'task-monitoring', 'orchestration'],
|
|
272
|
+
status: 'active',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch (regError) {
|
|
276
|
+
spinner.warn(chalk.yellow(`Agent registration failed: ${regError.message}`));
|
|
277
|
+
console.log(chalk.dim(' Continuing with local-only orchestration...'));
|
|
278
|
+
// Create local-only state
|
|
279
|
+
registerResponse = {
|
|
280
|
+
agentId: `local-${Date.now()}`,
|
|
281
|
+
name: orchestratorName,
|
|
282
|
+
capabilities: ['task-assignment', 'task-monitoring', 'orchestration'],
|
|
283
|
+
status: 'active',
|
|
284
|
+
organizationId: 'local',
|
|
285
|
+
createdAt: new Date().toISOString(),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Initialize context monitor for pressure tracking (TASK-9)
|
|
289
|
+
resetContextMonitor(); // Reset any previous session
|
|
290
|
+
const contextMonitor = getContextMonitor({
|
|
291
|
+
model: 'claude-opus-4-5-20251101', // Default to Opus for orchestrator
|
|
292
|
+
});
|
|
293
|
+
state = {
|
|
294
|
+
orchestratorId: registerResponse.agentId,
|
|
295
|
+
orchestratorName: registerResponse.name,
|
|
296
|
+
graphId: graphConfig.graphId,
|
|
297
|
+
sprintId: sprint.file,
|
|
298
|
+
startedAt: new Date(),
|
|
299
|
+
lastProgressAt: new Date(),
|
|
300
|
+
cyclesWithoutProgress: 0,
|
|
301
|
+
completedTasks: new Set(),
|
|
302
|
+
inProgressTasks: new Map(),
|
|
303
|
+
blockedTasks: new Set(),
|
|
304
|
+
assignmentHistory: [],
|
|
305
|
+
contextMonitor,
|
|
306
|
+
};
|
|
307
|
+
// Mark already-completed tasks from sprint file
|
|
308
|
+
for (const task of tasks) {
|
|
309
|
+
if (task.status === 'complete') {
|
|
310
|
+
state.completedTasks.add(task.id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
spinner.succeed(chalk.green(`Registered as ${chalk.bold(state.orchestratorName)}`));
|
|
314
|
+
console.log(chalk.dim(` Agent ID: ${state.orchestratorId}`));
|
|
315
|
+
}
|
|
316
|
+
// ============================================================
|
|
317
|
+
// PHASE 5: Start Heartbeat
|
|
318
|
+
// ============================================================
|
|
319
|
+
console.log('');
|
|
320
|
+
spinner = ora('Starting heartbeat...').start();
|
|
321
|
+
startHeartbeat(state.orchestratorId);
|
|
322
|
+
spinner.succeed(chalk.green('Heartbeat started (30s interval)'));
|
|
323
|
+
// ============================================================
|
|
324
|
+
// PHASE 6: Setup Graceful Shutdown
|
|
325
|
+
// ============================================================
|
|
326
|
+
const gracefulShutdown = async (signal) => {
|
|
327
|
+
if (isShuttingDown)
|
|
328
|
+
return;
|
|
329
|
+
isShuttingDown = true;
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log(chalk.yellow(`\n📡 Received ${signal}, shutting down gracefully...`));
|
|
332
|
+
// TASK-10: Save checkpoint with exit reason
|
|
333
|
+
if (state && stateManager) {
|
|
334
|
+
const checkpoint = stateManager.createCheckpoint(state);
|
|
335
|
+
await stateManager.saveCheckpoint(checkpoint, {
|
|
336
|
+
exitReason: 'user_interrupt',
|
|
337
|
+
exitCode: EXIT_CODE_SUCCESS,
|
|
338
|
+
});
|
|
339
|
+
console.log(chalk.dim(' Checkpoint saved for later resume'));
|
|
340
|
+
}
|
|
341
|
+
// Stop heartbeat
|
|
342
|
+
await shutdownHeartbeat();
|
|
343
|
+
console.log(chalk.green('✓ Orchestrator stopped'));
|
|
344
|
+
displayFinalStatus(state, tasks);
|
|
345
|
+
process.exit(EXIT_CODE_SUCCESS);
|
|
346
|
+
};
|
|
347
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
348
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
349
|
+
// ============================================================
|
|
350
|
+
// PHASE 7: Main Orchestration Loop
|
|
351
|
+
// ============================================================
|
|
352
|
+
console.log('');
|
|
353
|
+
console.log(chalk.bold.cyan('🔄 Starting orchestration loop...'));
|
|
354
|
+
console.log(chalk.dim(` Poll interval: ${options.pollInterval || DEFAULT_POLL_INTERVAL_SECONDS}s`));
|
|
355
|
+
console.log(chalk.dim(` Max runtime: ${options.maxRuntime || DEFAULT_MAX_RUNTIME_MINUTES} minutes`));
|
|
356
|
+
console.log('');
|
|
357
|
+
await runOrchestrationLoop(state, tasks, waves, {
|
|
358
|
+
pollInterval: (options.pollInterval || DEFAULT_POLL_INTERVAL_SECONDS) * 1000,
|
|
359
|
+
maxRuntime: (options.maxRuntime || DEFAULT_MAX_RUNTIME_MINUTES) * 60 * 1000,
|
|
360
|
+
verbose: options.verbose || false,
|
|
361
|
+
stateManager: stateManager, // TASK-10: Pass state manager for checkpoints
|
|
362
|
+
graphClient: client, // TASK-8: Pass graph client for state persistence
|
|
363
|
+
});
|
|
364
|
+
// ============================================================
|
|
365
|
+
// PHASE 8: Success Exit
|
|
366
|
+
// ============================================================
|
|
367
|
+
console.log('');
|
|
368
|
+
console.log(chalk.bold.green('🎉 All tasks completed!'));
|
|
369
|
+
displayFinalStatus(state, tasks);
|
|
370
|
+
// TASK-10: Delete checkpoint on successful completion
|
|
371
|
+
if (stateManager) {
|
|
372
|
+
await stateManager.deleteCheckpoint();
|
|
373
|
+
console.log(chalk.dim(' Checkpoint cleared (all tasks complete)'));
|
|
374
|
+
}
|
|
375
|
+
await shutdownHeartbeat();
|
|
376
|
+
process.exit(EXIT_CODE_SUCCESS);
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (spinner)
|
|
380
|
+
spinner.fail(chalk.red('Orchestration failed'));
|
|
381
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}`));
|
|
382
|
+
if (options.verbose && error.stack) {
|
|
383
|
+
console.error(chalk.dim(error.stack));
|
|
384
|
+
}
|
|
385
|
+
// TASK-10: Save checkpoint on error for recovery
|
|
386
|
+
if (state && stateManager) {
|
|
387
|
+
try {
|
|
388
|
+
const checkpoint = stateManager.createCheckpoint(state);
|
|
389
|
+
await stateManager.saveCheckpoint(checkpoint, {
|
|
390
|
+
exitReason: 'error',
|
|
391
|
+
exitCode: EXIT_CODE_ERROR,
|
|
392
|
+
});
|
|
393
|
+
console.log(chalk.dim(' Checkpoint saved for recovery'));
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// Ignore checkpoint save errors
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Cleanup
|
|
400
|
+
if (state) {
|
|
401
|
+
try {
|
|
402
|
+
await shutdownHeartbeat();
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Ignore cleanup errors
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function runOrchestrationLoop(state, tasks, waves, options) {
|
|
412
|
+
const client = options.graphClient;
|
|
413
|
+
let lastEventId = null;
|
|
414
|
+
const startTime = Date.now();
|
|
415
|
+
const { stateManager, graphClient } = options;
|
|
416
|
+
// TASK-8: Track last graph persistence time
|
|
417
|
+
const GRAPH_PERSIST_INTERVAL = 30 * 1000; // 30 seconds
|
|
418
|
+
let lastGraphPersist = Date.now();
|
|
419
|
+
while (true) {
|
|
420
|
+
// Check exit conditions
|
|
421
|
+
const incompleteTasks = tasks.filter(t => t.status !== 'complete').length;
|
|
422
|
+
if (incompleteTasks === 0) {
|
|
423
|
+
// All tasks complete!
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// Check max runtime
|
|
427
|
+
if (Date.now() - startTime > options.maxRuntime) {
|
|
428
|
+
console.log(chalk.yellow('\n⏰ Max runtime exceeded - checkpointing and exiting'));
|
|
429
|
+
// TASK-10: Save checkpoint with exit reason
|
|
430
|
+
const checkpoint = stateManager.createCheckpoint(state);
|
|
431
|
+
await stateManager.saveCheckpoint(checkpoint, {
|
|
432
|
+
exitReason: 'max_runtime',
|
|
433
|
+
exitCode: EXIT_CODE_RESPAWN,
|
|
434
|
+
});
|
|
435
|
+
console.log(chalk.dim(' Checkpoint saved for respawn'));
|
|
436
|
+
process.exit(EXIT_CODE_RESPAWN);
|
|
437
|
+
}
|
|
438
|
+
// TASK-9: Check context pressure (>80% triggers respawn)
|
|
439
|
+
const pressure = state.contextMonitor.getPressure();
|
|
440
|
+
if (state.contextMonitor.shouldRespawn()) {
|
|
441
|
+
console.log(chalk.magenta(`\n📊 Context pressure critical (${(pressure * 100).toFixed(1)}%) - checkpointing and respawning`));
|
|
442
|
+
// TASK-10: Save checkpoint with exit reason
|
|
443
|
+
const checkpoint = stateManager.createCheckpoint(state);
|
|
444
|
+
await stateManager.saveCheckpoint(checkpoint, {
|
|
445
|
+
exitReason: 'context_pressure',
|
|
446
|
+
exitCode: EXIT_CODE_RESPAWN,
|
|
447
|
+
});
|
|
448
|
+
console.log(chalk.dim(' Checkpoint saved for respawn'));
|
|
449
|
+
process.exit(EXIT_CODE_RESPAWN);
|
|
450
|
+
}
|
|
451
|
+
// TASK-9: Warn at 70% pressure (once per 5 minutes max)
|
|
452
|
+
if (state.contextMonitor.shouldWarn()) {
|
|
453
|
+
const now = new Date();
|
|
454
|
+
const shouldLog = !state.lastPressureWarning ||
|
|
455
|
+
(now.getTime() - state.lastPressureWarning.getTime()) > 5 * 60 * 1000;
|
|
456
|
+
if (shouldLog) {
|
|
457
|
+
const zone = state.contextMonitor.getZone();
|
|
458
|
+
const color = getPressureColor(zone);
|
|
459
|
+
console.log(chalk[color](`\n⚠️ Context pressure elevated: ${state.contextMonitor.formatMetrics()}`));
|
|
460
|
+
state.lastPressureWarning = now;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Check progress stall
|
|
464
|
+
if (state.cyclesWithoutProgress >= MAX_CYCLES_WITHOUT_PROGRESS) {
|
|
465
|
+
console.log(chalk.red('\n⚠️ No progress for 10 cycles - escalating'));
|
|
466
|
+
displayBlockers(state, tasks);
|
|
467
|
+
// TASK-10: Save checkpoint with exit reason
|
|
468
|
+
const checkpoint = stateManager.createCheckpoint(state);
|
|
469
|
+
await stateManager.saveCheckpoint(checkpoint, {
|
|
470
|
+
exitReason: 'no_progress',
|
|
471
|
+
exitCode: EXIT_CODE_ERROR,
|
|
472
|
+
});
|
|
473
|
+
console.log(chalk.dim(' Checkpoint saved for investigation'));
|
|
474
|
+
process.exit(EXIT_CODE_ERROR);
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
// Step 1: Discover available workers
|
|
478
|
+
const workers = await discoverWorkers();
|
|
479
|
+
// Step 2: Find available tasks (dependencies satisfied, not assigned)
|
|
480
|
+
const availableTasks = getAvailableOrchestratorTasks(tasks, state);
|
|
481
|
+
// Step 3: Match and assign tasks
|
|
482
|
+
const assignments = await assignTasks(state, availableTasks, workers, client, options.verbose);
|
|
483
|
+
if (assignments > 0) {
|
|
484
|
+
state.lastProgressAt = new Date();
|
|
485
|
+
state.cyclesWithoutProgress = 0;
|
|
486
|
+
}
|
|
487
|
+
// Step 4: Poll for completion events
|
|
488
|
+
const events = await pollCompletionEvents(state, client, lastEventId, options.pollInterval);
|
|
489
|
+
// Step 5: Process completion events
|
|
490
|
+
for (const event of events) {
|
|
491
|
+
await processCompletionEvent(state, tasks, event, options.verbose);
|
|
492
|
+
lastEventId = event.id;
|
|
493
|
+
state.lastProgressAt = new Date();
|
|
494
|
+
state.cyclesWithoutProgress = 0;
|
|
495
|
+
// TASK-9: Track event processing in context metrics
|
|
496
|
+
state.contextMonitor.recordEvent();
|
|
497
|
+
}
|
|
498
|
+
// TASK-9: Update context metrics for this cycle
|
|
499
|
+
// Estimate tokens from cycle activity (API calls, events, state updates)
|
|
500
|
+
const cycleTokenEstimate = 500 + (assignments * 200) + (events.length * 300);
|
|
501
|
+
state.contextMonitor.addTokens(cycleTokenEstimate);
|
|
502
|
+
// Update status display
|
|
503
|
+
if (options.verbose) {
|
|
504
|
+
displayCycleStatus(state, tasks, workers.length, availableTasks.length);
|
|
505
|
+
// TASK-9: Show context pressure in verbose mode
|
|
506
|
+
console.log(chalk.dim(` 📊 Context: ${state.contextMonitor.formatMetrics()}`));
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
displayProgressBar(state, tasks);
|
|
510
|
+
}
|
|
511
|
+
// Increment cycle counter if no progress
|
|
512
|
+
if (assignments === 0 && events.length === 0) {
|
|
513
|
+
state.cyclesWithoutProgress++;
|
|
514
|
+
}
|
|
515
|
+
// TASK-8: Persist state to graph every 30 seconds
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
if (now - lastGraphPersist >= GRAPH_PERSIST_INTERVAL) {
|
|
518
|
+
try {
|
|
519
|
+
const checkpoint = stateManager.createCheckpoint(state);
|
|
520
|
+
// Add persistence timestamp
|
|
521
|
+
checkpoint.persistedAt = new Date().toISOString();
|
|
522
|
+
checkpoint.recoveredFrom = state.recoveredFromCheckpointId;
|
|
523
|
+
// Persist to graph (non-blocking)
|
|
524
|
+
await persistStateToGraph(checkpoint, graphClient);
|
|
525
|
+
lastGraphPersist = now;
|
|
526
|
+
state.lastGraphPersist = new Date();
|
|
527
|
+
if (options.verbose) {
|
|
528
|
+
console.log(chalk.dim(` 💾 State persisted to graph`));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (persistError) {
|
|
532
|
+
// Log but don't fail - graph persistence is best-effort
|
|
533
|
+
if (options.verbose) {
|
|
534
|
+
console.log(chalk.yellow(` ⚠️ Graph persistence failed: ${persistError.message}`));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Wait before next cycle
|
|
539
|
+
await sleep(options.pollInterval);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
console.error(chalk.red(`\n⚠️ Error in orchestration cycle: ${error.message}`));
|
|
543
|
+
console.log(chalk.dim(' Retrying in 30 seconds...'));
|
|
544
|
+
await sleep(30000);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// ============================================================
|
|
549
|
+
// Worker Discovery
|
|
550
|
+
// ============================================================
|
|
551
|
+
async function discoverWorkers() {
|
|
552
|
+
try {
|
|
553
|
+
const response = await AgentClient.list({
|
|
554
|
+
status: 'active',
|
|
555
|
+
limit: 100,
|
|
556
|
+
});
|
|
557
|
+
// Filter out orchestrators (only get workers)
|
|
558
|
+
return response.agents.filter(agent => !agent.capabilities.includes('orchestration')).map(agent => ({
|
|
559
|
+
id: agent.id,
|
|
560
|
+
name: agent.name,
|
|
561
|
+
capabilities: agent.capabilities,
|
|
562
|
+
status: agent.status,
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
// Return empty array if can't discover workers
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// ============================================================
|
|
571
|
+
// Task Assignment
|
|
572
|
+
// ============================================================
|
|
573
|
+
function getAvailableOrchestratorTasks(tasks, state) {
|
|
574
|
+
return tasks.filter(task => {
|
|
575
|
+
// Must be pending (not complete, not assigned, not in progress)
|
|
576
|
+
if (task.status !== 'pending') {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
// Must not already be assigned
|
|
580
|
+
if (state.inProgressTasks.has(task.id)) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
// All dependencies must be complete
|
|
584
|
+
for (const depId of task.dependsOn) {
|
|
585
|
+
if (!state.completedTasks.has(depId)) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
async function assignTasks(state, availableTasks, workers, client, verbose) {
|
|
593
|
+
let assignments = 0;
|
|
594
|
+
// Get idle workers (not already assigned a task)
|
|
595
|
+
const busyAgentIds = new Set(state.inProgressTasks.values());
|
|
596
|
+
const idleWorkers = workers.filter(w => !busyAgentIds.has(w.id));
|
|
597
|
+
if (idleWorkers.length === 0 || availableTasks.length === 0) {
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
// Sort tasks by priority (higher first)
|
|
601
|
+
const sortedTasks = [...availableTasks].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
602
|
+
for (const task of sortedTasks) {
|
|
603
|
+
// TASK-8: Duplicate prevention - check if task already assigned
|
|
604
|
+
if (state.inProgressTasks.has(task.id)) {
|
|
605
|
+
if (verbose) {
|
|
606
|
+
console.log(chalk.dim(` Skipping ${task.id} - already assigned`));
|
|
607
|
+
}
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
// Find a capable worker
|
|
611
|
+
const capableWorker = idleWorkers.find(worker => task.requiredCapabilities.every(cap => worker.capabilities.includes(cap)) ||
|
|
612
|
+
task.requiredCapabilities.length === 0 // No specific requirements
|
|
613
|
+
);
|
|
614
|
+
if (!capableWorker) {
|
|
615
|
+
if (verbose) {
|
|
616
|
+
console.log(chalk.dim(` No capable worker for ${task.id}`));
|
|
617
|
+
}
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
// Assign task
|
|
621
|
+
try {
|
|
622
|
+
// TASK-8: Double-check not already assigned (race condition protection)
|
|
623
|
+
if (state.inProgressTasks.has(task.id)) {
|
|
624
|
+
if (verbose) {
|
|
625
|
+
console.log(chalk.dim(` Race condition detected for ${task.id} - skipping`));
|
|
626
|
+
}
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
// Note: This would call the assign API in production
|
|
630
|
+
// For now, we'll track locally
|
|
631
|
+
console.log(chalk.green(` → Assigned ${chalk.bold(task.id)} to ${capableWorker.name}`));
|
|
632
|
+
task.status = 'assigned';
|
|
633
|
+
task.assignedTo = capableWorker.id;
|
|
634
|
+
state.inProgressTasks.set(task.id, capableWorker.id);
|
|
635
|
+
state.assignmentHistory.push({
|
|
636
|
+
taskId: task.id,
|
|
637
|
+
agentId: capableWorker.id,
|
|
638
|
+
assignedAt: new Date(),
|
|
639
|
+
status: 'assigned',
|
|
640
|
+
});
|
|
641
|
+
// Remove worker from idle pool
|
|
642
|
+
const workerIndex = idleWorkers.indexOf(capableWorker);
|
|
643
|
+
if (workerIndex > -1) {
|
|
644
|
+
idleWorkers.splice(workerIndex, 1);
|
|
645
|
+
}
|
|
646
|
+
assignments++;
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
console.error(chalk.red(` Failed to assign ${task.id}: ${error.message}`));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return assignments;
|
|
653
|
+
}
|
|
654
|
+
async function pollCompletionEvents(state, client, lastEventId, timeout) {
|
|
655
|
+
// In production, this would call GET /api/v1/events/stream
|
|
656
|
+
// For now, return empty array (events would come from worker agents)
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
async function processCompletionEvent(state, tasks, event, verbose) {
|
|
660
|
+
const task = tasks.find(t => t.id === event.taskId);
|
|
661
|
+
if (!task)
|
|
662
|
+
return;
|
|
663
|
+
if (event.status === 'completed') {
|
|
664
|
+
console.log(chalk.green(` ✓ ${chalk.bold(event.taskId)} completed by ${event.agentId}`));
|
|
665
|
+
task.status = 'complete';
|
|
666
|
+
state.completedTasks.add(event.taskId);
|
|
667
|
+
state.inProgressTasks.delete(event.taskId);
|
|
668
|
+
// Update assignment history
|
|
669
|
+
const assignment = state.assignmentHistory.find(a => a.taskId === event.taskId && a.status === 'assigned');
|
|
670
|
+
if (assignment) {
|
|
671
|
+
assignment.status = 'completed';
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else if (event.status === 'failed' || event.status === 'released') {
|
|
675
|
+
console.log(chalk.yellow(` ⚠️ ${event.taskId} ${event.status} - returning to queue`));
|
|
676
|
+
task.status = 'pending';
|
|
677
|
+
task.assignedTo = undefined;
|
|
678
|
+
state.inProgressTasks.delete(event.taskId);
|
|
679
|
+
const assignment = state.assignmentHistory.find(a => a.taskId === event.taskId && a.status === 'assigned');
|
|
680
|
+
if (assignment) {
|
|
681
|
+
assignment.status = event.status === 'failed' ? 'failed' : 'released';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// ============================================================
|
|
686
|
+
// Display Functions
|
|
687
|
+
// ============================================================
|
|
688
|
+
function displayOrchestrationPlan(tasks, waves, stats, verbose) {
|
|
689
|
+
console.log(chalk.bold('\n📋 Orchestration Plan'));
|
|
690
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
691
|
+
// Summary
|
|
692
|
+
const completed = tasks.filter(t => t.status === 'complete').length;
|
|
693
|
+
const pending = tasks.filter(t => t.status === 'pending').length;
|
|
694
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress' || t.status === 'assigned').length;
|
|
695
|
+
console.log(` Total tasks: ${chalk.bold(tasks.length.toString())}`);
|
|
696
|
+
console.log(` Completed: ${chalk.green(completed.toString())}`);
|
|
697
|
+
console.log(` Pending: ${chalk.yellow(pending.toString())}`);
|
|
698
|
+
console.log(` In progress: ${chalk.cyan(inProgress.toString())}`);
|
|
699
|
+
console.log(` Execution waves: ${chalk.bold(waves.length.toString())}`);
|
|
700
|
+
if (verbose) {
|
|
701
|
+
console.log('');
|
|
702
|
+
console.log(chalk.bold(' Waves:'));
|
|
703
|
+
for (const wave of waves) {
|
|
704
|
+
console.log(` Wave ${wave.wave}: ${wave.tasks.map(t => t.id).join(', ')}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
708
|
+
}
|
|
709
|
+
function displayProgressBar(state, tasks) {
|
|
710
|
+
const total = tasks.length;
|
|
711
|
+
const completed = state.completedTasks.size;
|
|
712
|
+
const inProgress = state.inProgressTasks.size;
|
|
713
|
+
const pending = total - completed - inProgress;
|
|
714
|
+
const barWidth = 30;
|
|
715
|
+
const completedWidth = Math.round((completed / total) * barWidth);
|
|
716
|
+
const inProgressWidth = Math.round((inProgress / total) * barWidth);
|
|
717
|
+
const pendingWidth = barWidth - completedWidth - inProgressWidth;
|
|
718
|
+
const bar = chalk.green('█'.repeat(completedWidth)) +
|
|
719
|
+
chalk.cyan('▓'.repeat(inProgressWidth)) +
|
|
720
|
+
chalk.dim('░'.repeat(pendingWidth));
|
|
721
|
+
const percent = Math.round((completed / total) * 100);
|
|
722
|
+
// Clear line and print progress
|
|
723
|
+
process.stdout.write(`\r Progress: [${bar}] ${percent}% (${completed}/${total} complete, ${inProgress} in progress)`);
|
|
724
|
+
}
|
|
725
|
+
function displayCycleStatus(state, tasks, workerCount, availableTaskCount) {
|
|
726
|
+
console.log(chalk.dim(`\n Cycle: workers=${workerCount}, available=${availableTaskCount}, in_progress=${state.inProgressTasks.size}`));
|
|
727
|
+
}
|
|
728
|
+
function displayBlockers(state, tasks) {
|
|
729
|
+
console.log(chalk.bold('\n⚠️ Blockers:'));
|
|
730
|
+
const pendingTasks = tasks.filter(t => t.status === 'pending');
|
|
731
|
+
for (const task of pendingTasks) {
|
|
732
|
+
const missingDeps = task.dependsOn.filter(d => !state.completedTasks.has(d));
|
|
733
|
+
if (missingDeps.length > 0) {
|
|
734
|
+
console.log(chalk.yellow(` ${task.id} blocked by: ${missingDeps.join(', ')}`));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (state.inProgressTasks.size > 0) {
|
|
738
|
+
console.log(chalk.bold('\n In Progress (may be stuck):'));
|
|
739
|
+
for (const [taskId, agentId] of state.inProgressTasks) {
|
|
740
|
+
console.log(chalk.cyan(` ${taskId} → ${agentId}`));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function displayFinalStatus(state, tasks) {
|
|
745
|
+
const duration = Date.now() - state.startedAt.getTime();
|
|
746
|
+
const minutes = Math.floor(duration / 60000);
|
|
747
|
+
const seconds = Math.floor((duration % 60000) / 1000);
|
|
748
|
+
console.log(chalk.bold('\n📊 Final Status'));
|
|
749
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
750
|
+
console.log(` Duration: ${minutes}m ${seconds}s`);
|
|
751
|
+
console.log(` Completed: ${chalk.green(state.completedTasks.size.toString())}/${tasks.length}`);
|
|
752
|
+
console.log(` Assignments made: ${state.assignmentHistory.length}`);
|
|
753
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
754
|
+
}
|
|
755
|
+
// ============================================================
|
|
756
|
+
// Task Conversion
|
|
757
|
+
// ============================================================
|
|
758
|
+
function convertSprintTasks(sprint) {
|
|
759
|
+
const tasks = [];
|
|
760
|
+
for (const task of sprint.tasks) {
|
|
761
|
+
tasks.push({
|
|
762
|
+
id: task.id,
|
|
763
|
+
title: task.title,
|
|
764
|
+
description: task.title, // Could be enhanced with acceptance criteria
|
|
765
|
+
effort: task.effort,
|
|
766
|
+
priority: parsePriority(task.priority),
|
|
767
|
+
requiredCapabilities: extractCapabilities(task),
|
|
768
|
+
dependsOn: task.dependsOn || [],
|
|
769
|
+
status: mapSprintStatus(task.state),
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return tasks;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Parse priority string to number
|
|
776
|
+
*/
|
|
777
|
+
function parsePriority(priority) {
|
|
778
|
+
if (!priority)
|
|
779
|
+
return 0;
|
|
780
|
+
// Handle numeric strings
|
|
781
|
+
const numValue = parseInt(priority, 10);
|
|
782
|
+
if (!isNaN(numValue))
|
|
783
|
+
return numValue;
|
|
784
|
+
// Handle named priorities
|
|
785
|
+
switch (priority.toLowerCase()) {
|
|
786
|
+
case 'critical':
|
|
787
|
+
return 100;
|
|
788
|
+
case 'high':
|
|
789
|
+
return 75;
|
|
790
|
+
case 'medium':
|
|
791
|
+
return 50;
|
|
792
|
+
case 'low':
|
|
793
|
+
return 25;
|
|
794
|
+
default:
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function mapSprintStatus(state) {
|
|
799
|
+
switch (state) {
|
|
800
|
+
case 'complete':
|
|
801
|
+
return 'complete';
|
|
802
|
+
case 'in_progress':
|
|
803
|
+
return 'in_progress';
|
|
804
|
+
case 'paused':
|
|
805
|
+
return 'blocked';
|
|
806
|
+
default:
|
|
807
|
+
return 'pending';
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function mapStatusForDeps(status) {
|
|
811
|
+
switch (status) {
|
|
812
|
+
case 'complete':
|
|
813
|
+
return 'complete';
|
|
814
|
+
case 'in_progress':
|
|
815
|
+
case 'assigned':
|
|
816
|
+
return 'in_progress';
|
|
817
|
+
case 'blocked':
|
|
818
|
+
return 'blocked';
|
|
819
|
+
default:
|
|
820
|
+
return 'pending';
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function extractCapabilities(task) {
|
|
824
|
+
// Extract capabilities from task metadata or files
|
|
825
|
+
const capabilities = [];
|
|
826
|
+
// Infer from file extensions mentioned
|
|
827
|
+
const files = task.files || [];
|
|
828
|
+
for (const file of files) {
|
|
829
|
+
if (file.endsWith('.ts') || file.endsWith('.tsx')) {
|
|
830
|
+
if (!capabilities.includes('typescript')) {
|
|
831
|
+
capabilities.push('typescript');
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (file.endsWith('.test.ts') || file.endsWith('.spec.ts')) {
|
|
835
|
+
if (!capabilities.includes('testing')) {
|
|
836
|
+
capabilities.push('testing');
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (file.includes('/api/')) {
|
|
840
|
+
if (!capabilities.includes('api')) {
|
|
841
|
+
capabilities.push('api');
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return capabilities;
|
|
846
|
+
}
|
|
847
|
+
// ============================================================
|
|
848
|
+
// State Persistence (TASK-10: Moved to orchestrator-state.ts)
|
|
849
|
+
// ============================================================
|
|
850
|
+
// State persistence is now handled by OrchestratorStateManager
|
|
851
|
+
// ============================================================
|
|
852
|
+
// Utilities
|
|
853
|
+
// ============================================================
|
|
854
|
+
function sleep(ms) {
|
|
855
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
856
|
+
}
|
|
857
|
+
export default orchestrateCommand;
|
|
858
|
+
//# sourceMappingURL=orchestrate.js.map
|