agentxchain 2.112.0 → 2.113.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/bin/agentxchain.js +48 -1
- package/dashboard/app.js +2 -1
- package/dashboard/components/mission.js +180 -1
- package/package.json +1 -1
- package/src/commands/mission.js +529 -1
- package/src/lib/dashboard/bridge-server.js +9 -0
- package/src/lib/dashboard/file-watcher.js +10 -6
- package/src/lib/dashboard/plan-reader.js +108 -0
- package/src/lib/dashboard/state-reader.js +11 -0
- package/src/lib/mission-plans.js +520 -0
- package/src/lib/run-chain.js +5 -3
package/src/commands/mission.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { findProjectRoot } from '../lib/config.js';
|
|
2
|
+
import { findProjectRoot, loadProjectContext } from '../lib/config.js';
|
|
3
3
|
import {
|
|
4
4
|
attachChainToMission,
|
|
5
5
|
buildMissionListSummary,
|
|
@@ -10,6 +10,20 @@ import {
|
|
|
10
10
|
loadMissionArtifact,
|
|
11
11
|
loadMissionSnapshot,
|
|
12
12
|
} from '../lib/missions.js';
|
|
13
|
+
import {
|
|
14
|
+
approvePlanArtifact,
|
|
15
|
+
createPlanArtifact,
|
|
16
|
+
launchWorkstream,
|
|
17
|
+
markWorkstreamOutcome,
|
|
18
|
+
loadAllPlans,
|
|
19
|
+
loadLatestPlan,
|
|
20
|
+
loadPlan,
|
|
21
|
+
buildPlannerPrompt,
|
|
22
|
+
parsePlannerResponse,
|
|
23
|
+
validatePlannerOutput,
|
|
24
|
+
} from '../lib/mission-plans.js';
|
|
25
|
+
import { executeChainedRun } from '../lib/run-chain.js';
|
|
26
|
+
import { executeGovernedRun } from './run.js';
|
|
13
27
|
|
|
14
28
|
export async function missionStartCommand(opts) {
|
|
15
29
|
const root = findProjectRoot(opts.dir || process.cwd());
|
|
@@ -163,6 +177,520 @@ export async function missionAttachChainCommand(chainId, opts) {
|
|
|
163
177
|
renderMissionSnapshot(snapshot);
|
|
164
178
|
}
|
|
165
179
|
|
|
180
|
+
// ── Mission Plan Commands ────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* agentxchain mission plan [mission_id|latest] — generate a decomposition plan.
|
|
184
|
+
*
|
|
185
|
+
* Uses LLM-assisted one-shot generation with schema validation.
|
|
186
|
+
* Falls back to deterministic stub when no LLM is available (for testing).
|
|
187
|
+
*/
|
|
188
|
+
export async function missionPlanCommand(missionTarget, opts) {
|
|
189
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
190
|
+
if (!root) {
|
|
191
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Resolve mission target
|
|
196
|
+
const mission = missionTarget && missionTarget !== 'latest'
|
|
197
|
+
? loadMissionArtifact(root, missionTarget)
|
|
198
|
+
: loadLatestMissionArtifact(root);
|
|
199
|
+
|
|
200
|
+
if (!mission) {
|
|
201
|
+
if (missionTarget && missionTarget !== 'latest') {
|
|
202
|
+
console.error(chalk.red(`Mission not found: ${missionTarget}`));
|
|
203
|
+
} else {
|
|
204
|
+
console.error(chalk.red('No missions found.'));
|
|
205
|
+
console.error(chalk.dim(' Run `agentxchain mission start --title "..." --goal "..."` first.'));
|
|
206
|
+
}
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!mission.goal || !mission.goal.trim()) {
|
|
211
|
+
console.error(chalk.red(`Mission "${mission.mission_id}" has no goal text. The planner cannot operate on missing mission intent.`));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const constraints = Array.isArray(opts.constraint) ? opts.constraint : (opts.constraint ? [opts.constraint] : []);
|
|
216
|
+
const roleHints = Array.isArray(opts.roleHint) ? opts.roleHint : (opts.roleHint ? [opts.roleHint] : []);
|
|
217
|
+
|
|
218
|
+
// Build prompt and attempt LLM call, or use deterministic fallback
|
|
219
|
+
let plannerOutput;
|
|
220
|
+
const { systemPrompt, userPrompt } = buildPlannerPrompt(mission, constraints, roleHints);
|
|
221
|
+
|
|
222
|
+
if (opts._plannerOutput) {
|
|
223
|
+
// Injected planner output for testing — skip LLM call
|
|
224
|
+
plannerOutput = opts._plannerOutput;
|
|
225
|
+
} else {
|
|
226
|
+
// Attempt to use the configured api_proxy adapter for decomposition
|
|
227
|
+
try {
|
|
228
|
+
const { loadConfig } = await import('../lib/config.js');
|
|
229
|
+
const config = loadConfig(root);
|
|
230
|
+
const plannerConfig = config?.mission_planner || config?.api_proxy;
|
|
231
|
+
|
|
232
|
+
if (plannerConfig && plannerConfig.base_url && plannerConfig.model) {
|
|
233
|
+
const response = await callPlannerLLM(plannerConfig, systemPrompt, userPrompt);
|
|
234
|
+
const parsed = parsePlannerResponse(response);
|
|
235
|
+
if (!parsed.ok) {
|
|
236
|
+
console.error(chalk.red(`Planner response parse error: ${parsed.error}`));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
plannerOutput = parsed.data;
|
|
240
|
+
} else {
|
|
241
|
+
console.error(chalk.red('No mission planner or api_proxy configured.'));
|
|
242
|
+
console.error(chalk.dim(' Add "mission_planner" or "api_proxy" to agentxchain.json with base_url and model.'));
|
|
243
|
+
console.error(chalk.dim(' Or pass planner output via --planner-output-file <path> for offline use.'));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error(chalk.red(`Planner call failed: ${err.message}`));
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Validate and create plan artifact
|
|
253
|
+
const result = createPlanArtifact(root, mission, {
|
|
254
|
+
constraints,
|
|
255
|
+
roleHints,
|
|
256
|
+
plannerOutput,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
console.error(chalk.red('Plan validation failed:'));
|
|
261
|
+
for (const err of result.errors) {
|
|
262
|
+
console.error(chalk.red(` • ${err}`));
|
|
263
|
+
}
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (opts.json) {
|
|
268
|
+
console.log(JSON.stringify(result.plan, null, 2));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(chalk.green(`Created plan ${result.plan.plan_id} for mission ${mission.mission_id}`));
|
|
273
|
+
renderPlan(result.plan);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* agentxchain mission plan show [plan_id|latest] — show a decomposition plan.
|
|
278
|
+
*/
|
|
279
|
+
export async function missionPlanShowCommand(planTarget, opts) {
|
|
280
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
281
|
+
if (!root) {
|
|
282
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Resolve mission context
|
|
287
|
+
const mission = opts.mission
|
|
288
|
+
? loadMissionArtifact(root, opts.mission)
|
|
289
|
+
: loadLatestMissionArtifact(root);
|
|
290
|
+
|
|
291
|
+
if (!mission) {
|
|
292
|
+
console.error(chalk.red('No mission found.'));
|
|
293
|
+
console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Resolve plan target
|
|
298
|
+
const plan = planTarget && planTarget !== 'latest'
|
|
299
|
+
? loadPlan(root, mission.mission_id, planTarget)
|
|
300
|
+
: loadLatestPlan(root, mission.mission_id);
|
|
301
|
+
|
|
302
|
+
if (!plan) {
|
|
303
|
+
if (planTarget && planTarget !== 'latest') {
|
|
304
|
+
console.error(chalk.red(`Plan not found: ${planTarget}`));
|
|
305
|
+
} else {
|
|
306
|
+
console.log(chalk.dim('No plans found for this mission.'));
|
|
307
|
+
console.log(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (opts.json) {
|
|
313
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
renderPlan(plan);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* agentxchain mission plan list — list all plans for a mission.
|
|
322
|
+
*/
|
|
323
|
+
export async function missionPlanListCommand(opts) {
|
|
324
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
325
|
+
if (!root) {
|
|
326
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const mission = opts.mission
|
|
331
|
+
? loadMissionArtifact(root, opts.mission)
|
|
332
|
+
: loadLatestMissionArtifact(root);
|
|
333
|
+
|
|
334
|
+
if (!mission) {
|
|
335
|
+
console.error(chalk.red('No mission found.'));
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const plans = loadAllPlans(root, mission.mission_id);
|
|
340
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
|
|
341
|
+
const limited = plans.slice(0, limit);
|
|
342
|
+
|
|
343
|
+
if (opts.json) {
|
|
344
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (limited.length === 0) {
|
|
349
|
+
console.log(chalk.dim('No plans found for this mission.'));
|
|
350
|
+
console.log(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const header = [
|
|
355
|
+
pad('#', 4),
|
|
356
|
+
pad('Plan ID', 36),
|
|
357
|
+
pad('Status', 12),
|
|
358
|
+
pad('Workstreams', 12),
|
|
359
|
+
pad('Supersedes', 36),
|
|
360
|
+
pad('Created', 22),
|
|
361
|
+
].join(' ');
|
|
362
|
+
|
|
363
|
+
console.log(chalk.bold(header));
|
|
364
|
+
console.log(chalk.dim('─'.repeat(header.length)));
|
|
365
|
+
|
|
366
|
+
limited.forEach((plan, i) => {
|
|
367
|
+
console.log([
|
|
368
|
+
pad(String(i + 1), 4),
|
|
369
|
+
pad(plan.plan_id || '—', 36),
|
|
370
|
+
pad(formatPlanStatus(plan.status), 12),
|
|
371
|
+
pad(String(plan.workstreams?.length || 0), 12),
|
|
372
|
+
pad(plan.supersedes_plan_id || '—', 36),
|
|
373
|
+
pad(formatTimestamp(plan.created_at), 22),
|
|
374
|
+
].join(' '));
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
console.log(chalk.dim(`\n${limited.length} plan(s) shown`));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* agentxchain mission plan approve [plan_id|latest] — approve a decomposition plan.
|
|
382
|
+
*/
|
|
383
|
+
export async function missionPlanApproveCommand(planTarget, opts) {
|
|
384
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
385
|
+
if (!root) {
|
|
386
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const mission = opts.mission
|
|
391
|
+
? loadMissionArtifact(root, opts.mission)
|
|
392
|
+
: loadLatestMissionArtifact(root);
|
|
393
|
+
|
|
394
|
+
if (!mission) {
|
|
395
|
+
console.error(chalk.red('No mission found.'));
|
|
396
|
+
console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const plan = planTarget && planTarget !== 'latest'
|
|
401
|
+
? loadPlan(root, mission.mission_id, planTarget)
|
|
402
|
+
: loadLatestPlan(root, mission.mission_id);
|
|
403
|
+
|
|
404
|
+
if (!plan) {
|
|
405
|
+
if (planTarget && planTarget !== 'latest') {
|
|
406
|
+
console.error(chalk.red(`Plan not found: ${planTarget}`));
|
|
407
|
+
} else {
|
|
408
|
+
console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
|
|
409
|
+
console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
|
|
410
|
+
}
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const result = approvePlanArtifact(root, mission.mission_id, plan.plan_id);
|
|
415
|
+
if (!result.ok) {
|
|
416
|
+
console.error(chalk.red(result.error));
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
console.log(chalk.green(`Approved plan ${result.plan.plan_id} for mission ${mission.mission_id}`));
|
|
421
|
+
if (result.supersededPlanIds.length > 0) {
|
|
422
|
+
console.log(chalk.dim(` Superseded: ${result.supersededPlanIds.join(', ')}`));
|
|
423
|
+
}
|
|
424
|
+
renderPlan(result.plan);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* agentxchain mission plan launch [plan_id|latest] --workstream <id> — launch a workstream.
|
|
429
|
+
*
|
|
430
|
+
* Validates plan approval, workstream existence, dependency satisfaction.
|
|
431
|
+
* Records launch_record with workstream_id → chain_id binding.
|
|
432
|
+
*/
|
|
433
|
+
export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
434
|
+
const context = loadProjectContext(opts.dir || process.cwd());
|
|
435
|
+
if (!context) {
|
|
436
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
const { root } = context;
|
|
440
|
+
|
|
441
|
+
if (!opts.workstream) {
|
|
442
|
+
console.error(chalk.red('--workstream <id> is required. Specify which workstream to launch.'));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const mission = opts.mission
|
|
447
|
+
? loadMissionArtifact(root, opts.mission)
|
|
448
|
+
: loadLatestMissionArtifact(root);
|
|
449
|
+
|
|
450
|
+
if (!mission) {
|
|
451
|
+
console.error(chalk.red('No mission found.'));
|
|
452
|
+
console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const plan = planTarget && planTarget !== 'latest'
|
|
457
|
+
? loadPlan(root, mission.mission_id, planTarget)
|
|
458
|
+
: loadLatestPlan(root, mission.mission_id);
|
|
459
|
+
|
|
460
|
+
if (!plan) {
|
|
461
|
+
if (planTarget && planTarget !== 'latest') {
|
|
462
|
+
console.error(chalk.red(`Plan not found: ${planTarget}`));
|
|
463
|
+
} else {
|
|
464
|
+
console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
|
|
465
|
+
console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
|
|
466
|
+
}
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const launch = launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
|
|
471
|
+
if (!launch.ok) {
|
|
472
|
+
console.error(chalk.red(launch.error));
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const executor = opts._executeGovernedRun || executeGovernedRun;
|
|
477
|
+
const logger = opts._log || console.log;
|
|
478
|
+
const chainOpts = {
|
|
479
|
+
enabled: true,
|
|
480
|
+
maxChains: 0,
|
|
481
|
+
chainOn: ['completed'],
|
|
482
|
+
cooldownSeconds: 0,
|
|
483
|
+
mission: mission.mission_id,
|
|
484
|
+
chainId: launch.chainId,
|
|
485
|
+
};
|
|
486
|
+
const runOpts = {
|
|
487
|
+
autoApprove: !!opts.autoApprove,
|
|
488
|
+
provenance: {
|
|
489
|
+
trigger: 'manual',
|
|
490
|
+
created_by: 'operator',
|
|
491
|
+
trigger_reason: `mission:${mission.mission_id} workstream:${opts.workstream}`,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
let execution;
|
|
496
|
+
try {
|
|
497
|
+
execution = await executeChainedRun(context, runOpts, chainOpts, executor, logger);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, opts.workstream, {
|
|
500
|
+
terminalReason: 'execution_error',
|
|
501
|
+
completedAt: new Date().toISOString(),
|
|
502
|
+
});
|
|
503
|
+
console.error(chalk.red(`Workstream execution failed: ${error.message}`));
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const lastRun = execution?.chainReport?.runs?.[execution.chainReport.runs.length - 1] || null;
|
|
508
|
+
const terminalReason = lastRun?.status === 'completed'
|
|
509
|
+
? 'completed'
|
|
510
|
+
: (lastRun?.status || execution?.chainReport?.terminal_reason || 'execution_error');
|
|
511
|
+
const outcome = markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, opts.workstream, {
|
|
512
|
+
terminalReason,
|
|
513
|
+
completedAt: execution?.chainReport?.completed_at || new Date().toISOString(),
|
|
514
|
+
});
|
|
515
|
+
if (!outcome.ok) {
|
|
516
|
+
console.error(chalk.red(outcome.error));
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (opts.json) {
|
|
521
|
+
console.log(JSON.stringify({
|
|
522
|
+
workstream_id: opts.workstream,
|
|
523
|
+
chain_id: launch.chainId,
|
|
524
|
+
plan_id: launch.plan.plan_id,
|
|
525
|
+
mission_id: mission.mission_id,
|
|
526
|
+
launch_record: launch.launchRecord,
|
|
527
|
+
exit_code: execution.exitCode,
|
|
528
|
+
chain_terminal_reason: execution?.chainReport?.terminal_reason || null,
|
|
529
|
+
workstream_status: outcome.workstream.launch_status,
|
|
530
|
+
}, null, 2));
|
|
531
|
+
if (execution.exitCode !== 0) {
|
|
532
|
+
process.exit(execution.exitCode);
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
console.log(chalk.green(`Executed workstream ${chalk.bold(opts.workstream)} → chain ${chalk.bold(launch.chainId)}`));
|
|
538
|
+
console.log('');
|
|
539
|
+
console.log(chalk.dim(` Mission: ${mission.mission_id}`));
|
|
540
|
+
console.log(chalk.dim(` Plan: ${launch.plan.plan_id}`));
|
|
541
|
+
console.log(chalk.dim(` Chain ID: ${launch.chainId}`));
|
|
542
|
+
console.log(chalk.dim(` Outcome: ${outcome.workstream.launch_status}`));
|
|
543
|
+
console.log('');
|
|
544
|
+
renderPlan(outcome.plan);
|
|
545
|
+
if (execution.exitCode !== 0) {
|
|
546
|
+
console.error(chalk.red(`Workstream execution ended with exit code ${execution.exitCode}.`));
|
|
547
|
+
process.exit(execution.exitCode);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── Plan rendering ───────────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
function renderPlan(plan) {
|
|
554
|
+
console.log(chalk.bold(`Plan: ${plan.plan_id}`));
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log(` Mission: ${plan.mission_id}`);
|
|
557
|
+
console.log(` Status: ${formatPlanStatus(plan.status)}`);
|
|
558
|
+
console.log(` Goal: ${plan.input?.goal || '—'}`);
|
|
559
|
+
console.log(` Constraints: ${plan.input?.constraints?.length ? plan.input.constraints.join('; ') : 'none'}`);
|
|
560
|
+
console.log(` Role hints: ${plan.input?.role_hints?.length ? plan.input.role_hints.join(', ') : 'none'}`);
|
|
561
|
+
console.log(` Supersedes: ${plan.supersedes_plan_id || '—'}`);
|
|
562
|
+
if (plan.superseded_by_plan_id) {
|
|
563
|
+
console.log(` Superseded by:${plan.superseded_by_plan_id}`);
|
|
564
|
+
}
|
|
565
|
+
if (plan.approved_at) {
|
|
566
|
+
console.log(` Approved: ${plan.approved_at}`);
|
|
567
|
+
}
|
|
568
|
+
console.log(` Created: ${plan.created_at || '—'}`);
|
|
569
|
+
console.log('');
|
|
570
|
+
|
|
571
|
+
if (!plan.workstreams || plan.workstreams.length === 0) {
|
|
572
|
+
console.log(chalk.dim(' No workstreams.'));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const wsHeader = [
|
|
577
|
+
pad('#', 4),
|
|
578
|
+
pad('Workstream', 28),
|
|
579
|
+
pad('Status', 10),
|
|
580
|
+
pad('Roles', 20),
|
|
581
|
+
pad('Phases', 24),
|
|
582
|
+
pad('Depends On', 28),
|
|
583
|
+
'Title',
|
|
584
|
+
].join(' ');
|
|
585
|
+
|
|
586
|
+
console.log(chalk.bold(' Workstreams:'));
|
|
587
|
+
console.log(` ${chalk.dim(wsHeader)}`);
|
|
588
|
+
console.log(` ${chalk.dim('─'.repeat(wsHeader.length))}`);
|
|
589
|
+
|
|
590
|
+
plan.workstreams.forEach((ws, i) => {
|
|
591
|
+
const deps = Array.isArray(ws.depends_on) && ws.depends_on.length > 0
|
|
592
|
+
? ws.depends_on.join(', ')
|
|
593
|
+
: '—';
|
|
594
|
+
|
|
595
|
+
console.log(` ${[
|
|
596
|
+
pad(String(i + 1), 4),
|
|
597
|
+
pad(ws.workstream_id || '—', 28),
|
|
598
|
+
pad(formatLaunchStatus(ws.launch_status), 10),
|
|
599
|
+
pad((ws.roles || []).join(', '), 20),
|
|
600
|
+
pad((ws.phases || []).join(', '), 24),
|
|
601
|
+
pad(deps, 28),
|
|
602
|
+
ws.title || '—',
|
|
603
|
+
].join(' ')}`);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
if (plan.launch_records?.length) {
|
|
607
|
+
console.log('');
|
|
608
|
+
console.log(chalk.bold(' Launch records:'));
|
|
609
|
+
for (const rec of plan.launch_records) {
|
|
610
|
+
const statusTag = rec.status === 'completed' ? chalk.green('completed')
|
|
611
|
+
: rec.status === 'failed' ? chalk.red('failed')
|
|
612
|
+
: chalk.cyan('launched');
|
|
613
|
+
console.log(` ${chalk.cyan(rec.workstream_id)} → ${rec.chain_id} [${statusTag}]`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (plan.workstreams.some((ws) => ws.acceptance_checks?.length)) {
|
|
618
|
+
console.log('');
|
|
619
|
+
console.log(chalk.bold(' Acceptance checks:'));
|
|
620
|
+
plan.workstreams.forEach((ws) => {
|
|
621
|
+
if (ws.acceptance_checks?.length) {
|
|
622
|
+
console.log(` ${chalk.cyan(ws.workstream_id)}:`);
|
|
623
|
+
ws.acceptance_checks.forEach((check) => {
|
|
624
|
+
console.log(` • ${check}`);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function formatPlanStatus(status) {
|
|
632
|
+
if (!status) return '—';
|
|
633
|
+
switch (status) {
|
|
634
|
+
case 'proposed': return chalk.blue('proposed');
|
|
635
|
+
case 'approved': return chalk.green('approved');
|
|
636
|
+
case 'superseded': return chalk.dim('superseded');
|
|
637
|
+
case 'needs_attention': return chalk.yellow('needs_attention');
|
|
638
|
+
case 'completed': return chalk.green('completed');
|
|
639
|
+
default: return status;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function formatLaunchStatus(status) {
|
|
644
|
+
if (!status) return '—';
|
|
645
|
+
switch (status) {
|
|
646
|
+
case 'ready': return chalk.green('ready');
|
|
647
|
+
case 'blocked': return chalk.yellow('blocked');
|
|
648
|
+
case 'launched': return chalk.cyan('launched');
|
|
649
|
+
case 'completed': return chalk.green('completed');
|
|
650
|
+
case 'needs_attention': return chalk.red('attention');
|
|
651
|
+
default: return status;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ── LLM planner call ─────────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
async function callPlannerLLM(config, systemPrompt, userPrompt) {
|
|
658
|
+
const url = `${config.base_url.replace(/\/$/, '')}/chat/completions`;
|
|
659
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
660
|
+
if (config.api_key) {
|
|
661
|
+
headers['Authorization'] = `Bearer ${config.api_key}`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const body = {
|
|
665
|
+
model: config.model,
|
|
666
|
+
messages: [
|
|
667
|
+
{ role: 'system', content: systemPrompt },
|
|
668
|
+
{ role: 'user', content: userPrompt },
|
|
669
|
+
],
|
|
670
|
+
temperature: 0.3,
|
|
671
|
+
max_tokens: 4096,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const response = await fetch(url, {
|
|
675
|
+
method: 'POST',
|
|
676
|
+
headers,
|
|
677
|
+
body: JSON.stringify(body),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
const text = await response.text().catch(() => '');
|
|
682
|
+
throw new Error(`LLM API returned ${response.status}: ${text.slice(0, 200)}`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const json = await response.json();
|
|
686
|
+
const content = json?.choices?.[0]?.message?.content;
|
|
687
|
+
if (!content) {
|
|
688
|
+
throw new Error('LLM API returned no content in response.');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return content;
|
|
692
|
+
}
|
|
693
|
+
|
|
166
694
|
function renderMissionSnapshot(snapshot) {
|
|
167
695
|
console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
|
|
168
696
|
console.log('');
|
|
@@ -31,6 +31,7 @@ import { evaluateApprovalSlaReminders } from '../notification-runner.js';
|
|
|
31
31
|
import { readGateActionSnapshot } from './gate-action-reader.js';
|
|
32
32
|
import { readChainReportSnapshot } from './chain-report-reader.js';
|
|
33
33
|
import { readMissionSnapshot } from './mission-reader.js';
|
|
34
|
+
import { readPlanSnapshot } from './plan-reader.js';
|
|
34
35
|
|
|
35
36
|
const MIME_TYPES = {
|
|
36
37
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -478,6 +479,14 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
|
|
|
478
479
|
return;
|
|
479
480
|
}
|
|
480
481
|
|
|
482
|
+
if (pathname === '/api/plans') {
|
|
483
|
+
const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit'), 10) : undefined;
|
|
484
|
+
const missionId = url.searchParams.get('mission') || undefined;
|
|
485
|
+
const result = readPlanSnapshot(workspacePath, { limit, missionId });
|
|
486
|
+
writeJson(res, result.status, result.body);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
481
490
|
if (pathname === '/api/gate-actions') {
|
|
482
491
|
const result = readGateActionSnapshot(workspacePath);
|
|
483
492
|
writeJson(res, result.status, result.body);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { watch, existsSync } from 'fs';
|
|
9
9
|
import { basename, join } from 'path';
|
|
10
10
|
import { EventEmitter } from 'events';
|
|
11
|
-
import { WATCH_DIRECTORIES, resourcesForRelativePath } from './state-reader.js';
|
|
11
|
+
import { WATCH_DIRECTORIES, RECURSIVE_WATCH_DIRECTORIES, resourcesForRelativePath } from './state-reader.js';
|
|
12
12
|
|
|
13
13
|
const DEBOUNCE_MS = 100;
|
|
14
14
|
|
|
@@ -23,7 +23,7 @@ export class FileWatcher extends EventEmitter {
|
|
|
23
23
|
this.#agentxchainDir = agentxchainDir;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
#watchPath(relativeDir) {
|
|
26
|
+
#watchPath(relativeDir, { recursive = false } = {}) {
|
|
27
27
|
if (this.#watchers.has(relativeDir)) {
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
@@ -36,14 +36,15 @@ export class FileWatcher extends EventEmitter {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
|
-
const watcher = watch(watchPath, { recursive
|
|
39
|
+
const watcher = watch(watchPath, { recursive }, (eventType, filename) => {
|
|
40
40
|
if (!filename || this.#closed) return;
|
|
41
|
-
|
|
42
|
-
const
|
|
41
|
+
// For recursive watchers, filename includes subdirectory path
|
|
42
|
+
const fileSegment = recursive ? filename.replace(/\\/g, '/') : basename(filename);
|
|
43
|
+
const relativePath = relativeDir ? `${relativeDir}/${fileSegment}` : fileSegment;
|
|
43
44
|
const resources = resourcesForRelativePath(relativePath);
|
|
44
45
|
|
|
45
46
|
if (resources.length === 0) {
|
|
46
|
-
if (!relativeDir &&
|
|
47
|
+
if (!relativeDir && fileSegment === 'multirepo') {
|
|
47
48
|
this.#watchPath('multirepo');
|
|
48
49
|
}
|
|
49
50
|
return;
|
|
@@ -79,6 +80,9 @@ export class FileWatcher extends EventEmitter {
|
|
|
79
80
|
for (const relativeDir of WATCH_DIRECTORIES) {
|
|
80
81
|
this.#watchPath(relativeDir);
|
|
81
82
|
}
|
|
83
|
+
for (const relativeDir of RECURSIVE_WATCH_DIRECTORIES) {
|
|
84
|
+
this.#watchPath(relativeDir, { recursive: true });
|
|
85
|
+
}
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
stop() {
|