@jterrats/open-orchestra 0.4.0 → 0.4.2-beta.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/assets/web-console.js +91 -1
  3. package/dist/autonomous-phase-lifecycle.d.ts +26 -0
  4. package/dist/autonomous-phase-lifecycle.js +214 -0
  5. package/dist/autonomous-phase-lifecycle.js.map +1 -0
  6. package/dist/autonomous-run-state.d.ts +8 -0
  7. package/dist/autonomous-run-state.js +162 -0
  8. package/dist/autonomous-run-state.js.map +1 -0
  9. package/dist/autonomous-run-store.d.ts +17 -0
  10. package/dist/autonomous-run-store.js +89 -0
  11. package/dist/autonomous-run-store.js.map +1 -0
  12. package/dist/autonomous-workflow-constants.d.ts +7 -0
  13. package/dist/autonomous-workflow-constants.js +36 -0
  14. package/dist/autonomous-workflow-constants.js.map +1 -0
  15. package/dist/autonomous-workflow.d.ts +6 -45
  16. package/dist/autonomous-workflow.js +4 -394
  17. package/dist/autonomous-workflow.js.map +1 -1
  18. package/dist/benchmark.js +16 -0
  19. package/dist/benchmark.js.map +1 -1
  20. package/dist/clarification.d.ts +2 -0
  21. package/dist/clarification.js +23 -5
  22. package/dist/clarification.js.map +1 -1
  23. package/dist/cli.js +101 -14
  24. package/dist/cli.js.map +1 -1
  25. package/dist/command-manifest.js +8 -0
  26. package/dist/command-manifest.js.map +1 -1
  27. package/dist/command-utils.d.ts +6 -0
  28. package/dist/command-utils.js +19 -0
  29. package/dist/command-utils.js.map +1 -0
  30. package/dist/commands.d.ts +15 -43
  31. package/dist/commands.js +466 -912
  32. package/dist/commands.js.map +1 -1
  33. package/dist/constants.js +20 -0
  34. package/dist/constants.js.map +1 -1
  35. package/dist/defaults.d.ts +11 -0
  36. package/dist/defaults.js +11 -0
  37. package/dist/defaults.js.map +1 -1
  38. package/dist/instruction-commands.d.ts +5 -0
  39. package/dist/instruction-commands.js +98 -0
  40. package/dist/instruction-commands.js.map +1 -0
  41. package/dist/mcp-oauth-proxy.d.ts +47 -0
  42. package/dist/mcp-oauth-proxy.js +276 -0
  43. package/dist/mcp-oauth-proxy.js.map +1 -1
  44. package/dist/memory.d.ts +11 -0
  45. package/dist/memory.js +224 -0
  46. package/dist/memory.js.map +1 -0
  47. package/dist/metrics-commands.d.ts +3 -0
  48. package/dist/metrics-commands.js +123 -0
  49. package/dist/metrics-commands.js.map +1 -0
  50. package/dist/model-commands.d.ts +13 -0
  51. package/dist/model-commands.js +216 -0
  52. package/dist/model-commands.js.map +1 -0
  53. package/dist/notifications.d.ts +25 -0
  54. package/dist/notifications.js +187 -11
  55. package/dist/notifications.js.map +1 -1
  56. package/dist/package-update-check.d.ts +19 -0
  57. package/dist/package-update-check.js +123 -0
  58. package/dist/package-update-check.js.map +1 -0
  59. package/dist/runtime-adapters.d.ts +5 -1
  60. package/dist/runtime-adapters.js +27 -0
  61. package/dist/runtime-adapters.js.map +1 -1
  62. package/dist/runtime-bootstrap.js +4 -2
  63. package/dist/runtime-bootstrap.js.map +1 -1
  64. package/dist/runtime-commands.d.ts +9 -0
  65. package/dist/runtime-commands.js +156 -0
  66. package/dist/runtime-commands.js.map +1 -0
  67. package/dist/runtime-execution-adapters.d.ts +2 -0
  68. package/dist/runtime-execution-adapters.js +163 -0
  69. package/dist/runtime-execution-adapters.js.map +1 -0
  70. package/dist/runtime-execution-renderer.d.ts +10 -0
  71. package/dist/runtime-execution-renderer.js +113 -0
  72. package/dist/runtime-execution-renderer.js.map +1 -0
  73. package/dist/runtime-execution.d.ts +27 -0
  74. package/dist/runtime-execution.js +153 -0
  75. package/dist/runtime-execution.js.map +1 -0
  76. package/dist/skills-commands.d.ts +10 -0
  77. package/dist/skills-commands.js +138 -0
  78. package/dist/skills-commands.js.map +1 -0
  79. package/dist/skills.d.ts +7 -1
  80. package/dist/skills.js +120 -11
  81. package/dist/skills.js.map +1 -1
  82. package/dist/sprint-commands.d.ts +5 -0
  83. package/dist/sprint-commands.js +120 -0
  84. package/dist/sprint-commands.js.map +1 -0
  85. package/dist/subagent-protocol.js +6 -0
  86. package/dist/subagent-protocol.js.map +1 -1
  87. package/dist/telemetry-commands.d.ts +7 -0
  88. package/dist/telemetry-commands.js +82 -0
  89. package/dist/telemetry-commands.js.map +1 -0
  90. package/dist/tool-commands.d.ts +5 -0
  91. package/dist/tool-commands.js +159 -0
  92. package/dist/tool-commands.js.map +1 -0
  93. package/dist/types.d.ts +206 -4
  94. package/dist/types.js.map +1 -1
  95. package/dist/web-api.js +93 -1
  96. package/dist/web-api.js.map +1 -1
  97. package/dist/web-chart-contracts.d.ts +3 -1
  98. package/dist/web-chart-contracts.js +6 -0
  99. package/dist/web-chart-contracts.js.map +1 -1
  100. package/dist/web-console.js +2 -2
  101. package/dist/workflow-services.d.ts +21 -3
  102. package/dist/workflow-services.js +474 -10
  103. package/dist/workflow-services.js.map +1 -1
  104. package/docs/runtime-llm-flow.md +46 -0
  105. package/package.json +2 -1
  106. package/skills/proactive-orchestra/SKILL.md +27 -0
  107. package/skills/proactive-orchestra/manifest.json +41 -0
package/dist/commands.js CHANGED
@@ -1,31 +1,25 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { initWorkspace } from "./workspace.js";
1
+ import { appendEvent, initWorkspace } from "./workspace.js";
4
2
  import { requireArg } from "./args.js";
5
- import { AUTONOMOUS_PHASE_SEQUENCE, autonomousRunsPath, checkArchitectSizing, closePhase, createAutonomousRun, initPhase, listAutonomousRuns, markRunDone, markRunFailed, readAutonomousRun, resumePhaseIndex, suspendPhaseForClarification, resumePhaseFromClarification, } from "./autonomous-workflow.js";
6
- import { listClarifications, openClarification, answerClarification, } from "./clarification.js";
7
- import { computeBenchmark, computeVelocity, recordEstimate, summarizeBenchmark, } from "./benchmark.js";
8
- import { computeSprintBurndown, renderBurndownAscii } from "./burndown.js";
9
- import { calibrationReport, closeSprint, startSprint, velocityTrend, } from "./sprint-metrics.js";
3
+ import { TASK_STATUSES } from "./constants.js";
4
+ import { queryMemory, recordMemoryEvent, renderMemoryPacket, } from "./memory.js";
5
+ import { AUTONOMOUS_PHASE_SEQUENCE, autonomousRunsPath, checkArchitectSizing, closePhase, cancelRun, createAutonomousRun, initPhase, listActiveAutonomousRuns, listAutonomousRuns, markRunDone, markRunFailed, readAutonomousRun, resumePhaseIndex, suspendPhaseForClarification, resumePhaseFromClarification, } from "./autonomous-workflow.js";
6
+ import { listClarifications, listClarificationsByTask, openClarification, answerClarification, resolveClarificationRole, } from "./clarification.js";
10
7
  import { executePhaseWithLlm } from "./phase-executor.js";
11
- import { SIZING_LABELS } from "./types.js";
12
- import { resolveWorkspaceWritePath } from "./fs-utils.js";
13
- import { addEvidence, addPlaywrightEvidence, addTask, approveApproval, checkUsageBudget, checkReadiness, checkTaskDependencies, claimLock, completeWithProviderFallback, createHandoff, evaluateWorkflowGate, executeNextReadyTask, executePlanWithBudgetPreflight, executeReadyTaskBatch, generateExecutionPlan, generatePlaywrightTestPlan, generatePullRequestSummary, generateTaskGraphPlan, getUsageReport, getWorkflowStatus, getWorkflowSummary, getTaskContext, getWorkflowConfig, listEvidence, listConfiguredModelProviders, listDecisions, listLocks, listApprovals, listReviews, listRoles, listTasks, rejectApproval, recordReview, recordModelProvenance, recordDecision, releaseLock, setRoleModelProvider, showApproval, updateTask, listModelProvenance, } from "./workflow-services.js";
8
+ import { addEvidence, addPlaywrightEvidence, addTask, archiveTask, approveWorkflowGate, checkReadiness, checkTaskDependencies, claimLock, createHandoff, deleteTask, evaluateWorkflowGate, executeNextReadyTask, executePlanWithBudgetPreflight, executeReadyTaskBatch, generateExecutionPlan, generatePlaywrightTestPlan, generatePullRequestSummary, generateTaskGraphPlan, getWorkflowStatus, getWorkflowSummary, getTaskContext, getWorkflowConfig, listEvidence, listDecisions, listLocks, listReviews, listRoles, listTasks, previewTaskGraphRun, recordReview, recordDecision, releaseLock, updateTask, validatePreRun, } from "./workflow-services.js";
14
9
  import { validateWorkspace } from "./workspace-validator.js";
15
- import { addAgentLesson, listAgentLessons, listSkills, planSkillsForTask, promoteAgentLessons, readSourceOfTruth, recordSkillPlan, recordSkillRender, renderSkills, validateSkills, } from "./skills.js";
16
10
  import { getWebServerAddress, startWebApiServer } from "./web-api.js";
17
11
  import { decideTaskDelegation } from "./delegation-decision.js";
18
- import { lintMermaidDiagram, recordDiagramLintEvidence, } from "./diagram-validation.js";
19
- import { evaluateMcpOAuthProxy, } from "./mcp-oauth-proxy.js";
20
- import { detectStaleInstructionFilesFromManifest, resolveInstructionImportsFromFile, updateManagedInstructionFile, } from "./instruction-updates.js";
21
- import { applyInstructionUpdatesFromManifest } from "./instruction-apply.js";
22
- import { renderSubagentProtocol, upsertSubagentProtocolBlock, } from "./subagent-protocol.js";
23
12
  import { listCollaborationFlows, recommendCollaborationFlow, } from "./collaboration-flows.js";
24
13
  import { listWorkflowTemplates, renderWorkflowTemplates, selectWorkflowTemplates, validateWorkflowTemplates, } from "./workflow-templates.js";
25
- import { listCommandManifest } from "./command-manifest.js";
26
- import { listRuntimeAdapters, parseRuntimeTarget } from "./runtime-adapters.js";
27
- import { renderRuntimeBootstrap, upsertRuntimeBootstrapBlock, } from "./runtime-bootstrap.js";
28
- import { disableTelemetryConsent, enableTelemetryConsent, exportTelemetryDataset, exportTelemetryEvalDataset, getTelemetryConsent, parseTelemetryLevel, requiresSensitiveTelemetryOptIn, submitTelemetryDataset, } from "./telemetry.js";
14
+ import { parseRuntimeTarget } from "./runtime-adapters.js";
15
+ export { instructionsApplyCommand, instructionsBlockCommand, instructionsImportsCommand, instructionsStaleCommand, } from "./instruction-commands.js";
16
+ export { benchmarkCommand, estimateCommand } from "./metrics-commands.js";
17
+ export { burndownCommand, calibrationCommand, sprintCommand, velocityCommand, } from "./sprint-commands.js";
18
+ export { approvalsApproveCommand, approvalsListCommand, approvalsRejectCommand, approvalsShowCommand, budgetCheckCommand, configShowCommand, modelCompleteFakeCommand, modelProvidersCommand, modelProvenanceAddCommand, modelProvenanceListCommand, modelSetRoleCommand, usageCommand, } from "./model-commands.js";
19
+ export { commandsManifestCommand, protocolBlockCommand, protocolRenderCommand, runtimeAdaptersCommand, runtimeBootstrapCommand, runtimeBriefCommand, runtimeDelegatePlanCommand, runtimeHandoffCommand, } from "./runtime-commands.js";
20
+ export { lessonsAddCommand, lessonsDeleteCommand, lessonsListCommand, lessonsPromoteCommand, skillsListCommand, skillsPlanCommand, skillsRenderCommand, skillsValidateCommand, sourcesListCommand, } from "./skills-commands.js";
21
+ export { diagramsLintCommand, mcpOAuthProxyEvaluateCommand, mcpOAuthProxyStartCommand, mcpOAuthProxyTokenCommand, } from "./tool-commands.js";
22
+ export { telemetryDisableCommand, telemetryEnableCommand, telemetryEvalDatasetCommand, telemetryExportCommand, telemetryStatusCommand, telemetrySubmitCommand, } from "./telemetry-commands.js";
29
23
  import { buildPrBody, createPullRequest } from "./github.js";
30
24
  export async function initCommand(options, io) {
31
25
  const root = stringOption(options["target-dir"]) ?? process.cwd();
@@ -71,6 +65,23 @@ export async function statusCommand(options, io) {
71
65
  }
72
66
  }
73
67
  export async function validateCommand(options, io) {
68
+ if (options["pre-run"]) {
69
+ const report = await validatePreRun(requireArg(options, "task"), removeUndefined({
70
+ bypass: Boolean(options.bypass),
71
+ bypassOwner: stringOption(options["bypass-owner"]),
72
+ bypassRationale: stringOption(options["bypass-rationale"]),
73
+ }));
74
+ if (options.json) {
75
+ io.log(JSON.stringify(report, null, 2));
76
+ }
77
+ else {
78
+ io.log(renderPreRunValidation(report));
79
+ }
80
+ if (!report.allowed) {
81
+ throw new Error(`pre-run validation failed: ${report.missing.join(", ")}`);
82
+ }
83
+ return;
84
+ }
74
85
  const report = await validateWorkspace();
75
86
  if (options.json) {
76
87
  io.log(JSON.stringify(report, null, 2));
@@ -122,7 +133,11 @@ export async function taskAddCommand(options, io) {
122
133
  io.log(`Added task ${task.id}`);
123
134
  }
124
135
  export async function taskListCommand(options, io) {
125
- const tasks = await listTasks();
136
+ if (options.help || options.h) {
137
+ io.log("task list [--json] [--status <csv>] [--owner <role>] [--filter <text>] [--top-level] [--include-archived]");
138
+ return;
139
+ }
140
+ const tasks = filterTasks(await listTasks(), options);
126
141
  if (options.json) {
127
142
  io.log(JSON.stringify(tasks, null, 2));
128
143
  return;
@@ -135,6 +150,67 @@ export async function taskListCommand(options, io) {
135
150
  io.log(`${task.id} [${task.status}] ${task.title} (${task.ownerRole})`);
136
151
  }
137
152
  }
153
+ function filterTasks(tasks, options) {
154
+ const statuses = parseTaskStatusFilter(options.status);
155
+ const owner = stringOption(options.owner);
156
+ const textFilter = stringOption(options.filter)?.toLowerCase();
157
+ return tasks.filter((task) => matchesStatus(task, statuses) &&
158
+ (options["include-archived"] || task.status !== "archived") &&
159
+ (!owner || task.ownerRole === owner) &&
160
+ (!textFilter || taskSearchText(task).includes(textFilter)) &&
161
+ (!options["top-level"] || isTopLevelTask(task)));
162
+ }
163
+ function parseTaskStatusFilter(value) {
164
+ const statuses = parseCsv(value);
165
+ const invalid = statuses.filter((status) => !TASK_STATUSES.includes(status));
166
+ if (invalid.length > 0) {
167
+ throw new Error(`unknown task status: ${invalid.join(", ")}. Valid statuses: ${TASK_STATUSES.join(", ")}`);
168
+ }
169
+ return new Set(statuses);
170
+ }
171
+ function matchesStatus(task, statuses) {
172
+ return statuses.size === 0 || statuses.has(task.status);
173
+ }
174
+ function taskSearchText(task) {
175
+ return `${task.id} ${task.title}`.toLowerCase();
176
+ }
177
+ function isTopLevelTask(task) {
178
+ return !["pm", "po", "architect", "developer", "qa", "release"].some((phase) => task.id.includes(`-${phase}-`));
179
+ }
180
+ export async function taskShowCommand(options, io) {
181
+ const id = requireArg(options, "id");
182
+ const task = (await listTasks()).find((candidate) => candidate.id === id);
183
+ if (!task) {
184
+ throw new Error(`unknown task: ${id}`);
185
+ }
186
+ if (options.json) {
187
+ io.log(JSON.stringify(task, null, 2));
188
+ return;
189
+ }
190
+ for (const line of renderTaskDetails(task)) {
191
+ io.log(line);
192
+ }
193
+ }
194
+ export async function taskDeleteCommand(options, io) {
195
+ const task = await deleteTask(requireArg(options, "id"), {
196
+ force: Boolean(options.force),
197
+ });
198
+ if (options.json) {
199
+ io.log(JSON.stringify(task, null, 2));
200
+ return;
201
+ }
202
+ io.log(`Deleted task ${task.id}`);
203
+ }
204
+ export async function taskArchiveCommand(options, io) {
205
+ const task = await archiveTask(requireArg(options, "id"), {
206
+ force: Boolean(options.force),
207
+ });
208
+ if (options.json) {
209
+ io.log(JSON.stringify(task, null, 2));
210
+ return;
211
+ }
212
+ io.log(`Archived task ${task.id}`);
213
+ }
138
214
  export async function rolesListCommand(options, io) {
139
215
  const roles = await listRoles();
140
216
  if (options.json) {
@@ -154,6 +230,15 @@ export async function taskUpdateCommand(options, io) {
154
230
  const id = requireArg(options, "id");
155
231
  await updateTask(removeUndefined({
156
232
  id,
233
+ title: stringOption(options.title),
234
+ ownerRole: stringOption(options.owner),
235
+ goal: stringOption(options.goal),
236
+ scope: stringOption(options.scope),
237
+ paths: csvOption(options.paths),
238
+ acceptanceCriteria: singleValueArrayOption(options.acceptance),
239
+ assumptions: singleValueArrayOption(options.assumptions),
240
+ risks: singleValueArrayOption(options.risks),
241
+ testStrategy: stringOption(options["test-strategy"]),
157
242
  status: stringOption(options.status),
158
243
  blockedReason: stringOption(options["blocked-reason"]),
159
244
  }));
@@ -176,6 +261,15 @@ export async function graphPlanCommand(options, io) {
176
261
  io.log(renderTaskGraphPlan(plan));
177
262
  }
178
263
  export async function graphRunNextCommand(options, io) {
264
+ if (options["dry-run"]) {
265
+ const preview = await previewTaskGraphRun("run-next");
266
+ if (options.json) {
267
+ io.log(JSON.stringify(preview, null, 2));
268
+ return;
269
+ }
270
+ io.log(renderTaskGraphDryRunPreview(preview));
271
+ return;
272
+ }
179
273
  const run = await executeNextReadyTask();
180
274
  if (options.json) {
181
275
  io.log(JSON.stringify(run, null, 2));
@@ -184,6 +278,15 @@ export async function graphRunNextCommand(options, io) {
184
278
  io.log(renderExecutionRunMarkdown(run));
185
279
  }
186
280
  export async function graphRunReadyCommand(options, io) {
281
+ if (options["dry-run"]) {
282
+ const preview = await previewTaskGraphRun("run-ready");
283
+ if (options.json) {
284
+ io.log(JSON.stringify(preview, null, 2));
285
+ return;
286
+ }
287
+ io.log(renderTaskGraphDryRunPreview(preview));
288
+ return;
289
+ }
187
290
  const batch = await executeReadyTaskBatch();
188
291
  if (options.json) {
189
292
  io.log(JSON.stringify(batch, null, 2));
@@ -256,6 +359,7 @@ export async function handoffCommand(options, io) {
256
359
  gaps: stringOption(options.gaps),
257
360
  risks: stringOption(options.risks),
258
361
  playwright: stringOption(options.playwright),
362
+ updateOwner: options["update-owner"] ? true : undefined,
259
363
  }));
260
364
  io.log(`Created ${artifact}`);
261
365
  }
@@ -349,56 +453,6 @@ export async function summaryCommand(options, io) {
349
453
  }
350
454
  io.log(renderSummaryMarkdown(summary));
351
455
  }
352
- export async function usageCommand(options, io) {
353
- const report = await getUsageReport(stringOption(options.task));
354
- if (options.json) {
355
- io.log(JSON.stringify(report, null, 2));
356
- return;
357
- }
358
- io.log(renderUsageReport(report));
359
- }
360
- export async function budgetCheckCommand(options, io) {
361
- const result = await checkUsageBudget(stringOption(options.task));
362
- if (options.json) {
363
- io.log(JSON.stringify(result, null, 2));
364
- }
365
- else {
366
- io.log(renderBudgetCheck(result));
367
- }
368
- if (!result.passed) {
369
- throw new Error("budget check failed");
370
- }
371
- }
372
- export async function approvalsListCommand(options, io) {
373
- const approvals = await listApprovals(stringOption(options.task));
374
- if (options.json) {
375
- io.log(JSON.stringify(approvals, null, 2));
376
- return;
377
- }
378
- if (approvals.length === 0) {
379
- io.log("No approvals");
380
- return;
381
- }
382
- for (const approval of approvals) {
383
- io.log(`${approval.id} [${approval.status}] ${approval.taskId ?? "no-task"} ${approval.artifact}`);
384
- }
385
- }
386
- export async function approvalsShowCommand(options, io) {
387
- const approval = await showApproval(requireArg(options, "id"));
388
- if (options.json) {
389
- io.log(JSON.stringify(approval, null, 2));
390
- return;
391
- }
392
- io.log(approval.content);
393
- }
394
- export async function approvalsApproveCommand(options, io) {
395
- const approval = await approveApproval(parseApprovalDecision(options));
396
- io.log(`Approved ${approval.id}`);
397
- }
398
- export async function approvalsRejectCommand(options, io) {
399
- const approval = await rejectApproval(parseApprovalDecision(options));
400
- io.log(`Rejected ${approval.id}`);
401
- }
402
456
  export async function prSummaryCommand(options, io) {
403
457
  const summary = await generatePullRequestSummary(requireArg(options, "task"));
404
458
  if (options.json) {
@@ -425,6 +479,40 @@ export async function contextCommand(options, io) {
425
479
  }
426
480
  io.log(renderTaskContextMarkdown(context));
427
481
  }
482
+ export async function memoryQueryCommand(options, io) {
483
+ const hook = parseMemoryHook(stringOption(options.hook));
484
+ const taskId = stringOption(options.task);
485
+ const query = stringOption(options.query);
486
+ const packet = await queryMemory({
487
+ ...(taskId ? { taskId } : {}),
488
+ hook,
489
+ ...(query ? { query } : {}),
490
+ tokenBudget: numberOption(options.budget, 900),
491
+ });
492
+ await recordMemoryEvent(process.cwd(), packet, "MEMORY_QUERIED");
493
+ if (options.json) {
494
+ io.log(JSON.stringify(packet, null, 2));
495
+ return;
496
+ }
497
+ io.log(renderMemoryPacket(packet));
498
+ }
499
+ export async function memoryHookCommand(options, io) {
500
+ const point = parseMemoryHook(requireArg(options, "point"));
501
+ const taskId = stringOption(options.task);
502
+ const query = stringOption(options.query);
503
+ const packet = await queryMemory({
504
+ ...(taskId ? { taskId } : {}),
505
+ hook: point,
506
+ ...(query ? { query } : {}),
507
+ tokenBudget: numberOption(options.budget, 900),
508
+ });
509
+ await recordMemoryEvent(process.cwd(), packet, "MEMORY_HOOK_RAN");
510
+ if (options.json) {
511
+ io.log(JSON.stringify(packet, null, 2));
512
+ return;
513
+ }
514
+ io.log(renderMemoryPacket(packet));
515
+ }
428
516
  export async function planCommand(options, io) {
429
517
  const plan = await generateExecutionPlan(requireArg(options, "task"));
430
518
  if (options.json) {
@@ -435,7 +523,12 @@ export async function planCommand(options, io) {
435
523
  }
436
524
  export async function runCommand(options, io) {
437
525
  const taskId = requireArg(options, "task");
438
- const run = await executePlanWithBudgetPreflight(taskId, process.cwd(), parseBudgetEscalationDecision(options));
526
+ const run = await executePlanWithBudgetPreflight(taskId, process.cwd(), parseBudgetEscalationDecision(options), removeUndefined({
527
+ estimatedCostUsd: stringOption(options["estimate-cost"])
528
+ ? numberOption(options["estimate-cost"], 0)
529
+ : undefined,
530
+ override: options.yes ? true : undefined,
531
+ }));
439
532
  if (options.json) {
440
533
  io.log(JSON.stringify(run, null, 2));
441
534
  return;
@@ -460,254 +553,6 @@ export async function playwrightEvidenceCommand(options, io) {
460
553
  }));
461
554
  io.log(`Created ${artifact}`);
462
555
  }
463
- export async function configShowCommand(options, io) {
464
- const config = await getWorkflowConfig();
465
- if (options.json) {
466
- io.log(JSON.stringify(config, null, 2));
467
- return;
468
- }
469
- io.log(`Tools: ${Object.keys(config.tools ?? {}).length}`);
470
- io.log(`Role routes: ${Object.keys(config.providers?.byRole ?? {}).length}`);
471
- }
472
- export async function modelProvidersCommand(options, io) {
473
- const providers = await listConfiguredModelProviders();
474
- if (options.json) {
475
- io.log(JSON.stringify(providers, null, 2));
476
- return;
477
- }
478
- if (providers.length === 0) {
479
- io.log("No configured model providers");
480
- return;
481
- }
482
- for (const provider of providers) {
483
- io.log(`${provider.scope}: ${provider.provider}/${provider.model} (${provider.timeoutMs}ms)`);
484
- }
485
- }
486
- export async function modelSetRoleCommand(options, io) {
487
- const role = requireArg(options, "role");
488
- const routing = parseProviderRouting(options);
489
- await setRoleModelProvider(role, routing);
490
- io.log(`Configured model provider for ${role}`);
491
- }
492
- export async function modelProvenanceAddCommand(options, io) {
493
- const record = await recordModelProvenance({
494
- task: requireArg(options, "task"),
495
- role: requireArg(options, "role"),
496
- provider: requireArg(options, "provider"),
497
- model: requireArg(options, "model"),
498
- promptId: requireArg(options, "prompt-id"),
499
- responseId: requireArg(options, "response-id"),
500
- inputTokens: numberOption(options["input-tokens"], 0),
501
- outputTokens: numberOption(options["output-tokens"], 0),
502
- estimatedCostUsd: numberOption(options["estimated-cost-usd"], 0),
503
- finishReason: requireArg(options, "finish-reason"),
504
- });
505
- io.log(`Recorded model provenance ${record.responseId}`);
506
- }
507
- export async function modelProvenanceListCommand(options, io) {
508
- const records = await listModelProvenance(stringOption(options.task));
509
- if (options.json) {
510
- io.log(JSON.stringify(records, null, 2));
511
- return;
512
- }
513
- if (records.length === 0) {
514
- io.log("No model provenance");
515
- return;
516
- }
517
- for (const record of records) {
518
- io.log(`${record.task} ${record.role} ${record.provider}/${record.model} ${record.responseId}`);
519
- }
520
- }
521
- export async function modelCompleteFakeCommand(options, io) {
522
- const result = await completeWithProviderFallback({
523
- provider: requireArg(options, "provider"),
524
- model: requireArg(options, "model"),
525
- fallbacks: parseCsv(options.fallbacks),
526
- maxTokens: 0,
527
- maxCostUsd: 0,
528
- timeoutMs: 30000,
529
- retries: 0,
530
- requiredCapabilities: [],
531
- }, requireArg(options, "prompt"), removeUndefined({
532
- failingProviders: parseCsv(options["fail-provider"]),
533
- taskId: stringOption(options.task),
534
- role: stringOption(options.role) ?? "parent",
535
- }));
536
- if (options.json) {
537
- io.log(JSON.stringify(result, null, 2));
538
- return;
539
- }
540
- io.log(`Fake completion used ${result.provider}/${result.model} fallback=${String(result.fallbackUsed)}`);
541
- }
542
- export async function telemetryStatusCommand(options, io) {
543
- const telemetry = await getTelemetryConsent();
544
- if (options.json) {
545
- io.log(JSON.stringify(telemetry, null, 2));
546
- return;
547
- }
548
- io.log("Telemetry: " + (telemetry.enabled ? "enabled" : "off"));
549
- io.log("Level: " + telemetry.level);
550
- io.log("Policy: " + telemetry.policyVersion);
551
- }
552
- export async function telemetryEnableCommand(options, io) {
553
- const level = parseTelemetryLevel(stringOption(options.level) ?? "metadata");
554
- if (requiresSensitiveTelemetryOptIn(level) && !options["confirm-sensitive"]) {
555
- throw new Error("prompt-sample and eval-dataset telemetry require --confirm-sensitive");
556
- }
557
- const telemetry = await enableTelemetryConsent({
558
- level,
559
- actor: stringOption(options.actor) ?? "user",
560
- });
561
- if (options.json) {
562
- io.log(JSON.stringify(telemetry, null, 2));
563
- return;
564
- }
565
- io.log("Telemetry enabled at level " + telemetry.level);
566
- }
567
- export async function telemetryExportCommand(options, io) {
568
- const result = await exportTelemetryDataset({
569
- dryRun: Boolean(options["dry-run"]),
570
- });
571
- if (options.json) {
572
- io.log(JSON.stringify(result, null, 2));
573
- return;
574
- }
575
- io.log((result.dryRun ? "Telemetry export dry run" : "Telemetry exported") +
576
- ": " +
577
- result.recordCount +
578
- " record(s)");
579
- if (result.file) {
580
- io.log("File: " + result.file);
581
- }
582
- }
583
- export async function telemetrySubmitCommand(options, io) {
584
- const result = await submitTelemetryDataset({
585
- file: requireArg(options, "file"),
586
- endpoint: requireArg(options, "endpoint"),
587
- });
588
- if (options.json) {
589
- io.log(JSON.stringify(result, null, 2));
590
- return;
591
- }
592
- io.log("Telemetry submission recorded: " + result.fileHash);
593
- }
594
- export async function telemetryEvalDatasetCommand(options, io) {
595
- const result = await exportTelemetryEvalDataset({
596
- dryRun: Boolean(options["dry-run"]),
597
- });
598
- if (options.json) {
599
- io.log(JSON.stringify(result, null, 2));
600
- return;
601
- }
602
- io.log((result.dryRun ? "Eval dataset dry run" : "Eval dataset exported") +
603
- ": " +
604
- result.recordCount +
605
- " record(s)");
606
- if (result.file) {
607
- io.log("File: " + result.file);
608
- }
609
- }
610
- export async function telemetryDisableCommand(options, io) {
611
- const telemetry = await disableTelemetryConsent({
612
- actor: stringOption(options.actor) ?? "user",
613
- });
614
- if (options.json) {
615
- io.log(JSON.stringify(telemetry, null, 2));
616
- return;
617
- }
618
- io.log("Telemetry disabled");
619
- }
620
- export async function instructionsApplyCommand(options, io) {
621
- const report = await applyInstructionUpdatesFromManifest({
622
- manifestPath: requireArg(options, "manifest"),
623
- check: Boolean(options.check),
624
- dryRun: Boolean(options["dry-run"]),
625
- force: Boolean(options.force),
626
- });
627
- if (options.json) {
628
- io.log(JSON.stringify(report, null, 2));
629
- return;
630
- }
631
- io.log("Instruction update " +
632
- report.mode +
633
- ": " +
634
- report.totals.changed +
635
- " changed, " +
636
- report.totals.unchanged +
637
- " unchanged, " +
638
- report.totals.blocked +
639
- " blocked");
640
- for (const item of report.items) {
641
- io.log(item.status +
642
- " " +
643
- item.filePath +
644
- "#" +
645
- item.blockId +
646
- " - " +
647
- item.reason);
648
- }
649
- }
650
- export async function instructionsStaleCommand(options, io) {
651
- const report = await detectStaleInstructionFilesFromManifest({
652
- manifestPath: requireArg(options, "manifest"),
653
- });
654
- if (options.json) {
655
- io.log(JSON.stringify(report, null, 2));
656
- return;
657
- }
658
- io.log("Instruction file status:");
659
- for (const item of report.items) {
660
- io.log(item.status +
661
- " " +
662
- item.filePath +
663
- "#" +
664
- item.blockId +
665
- " - " +
666
- item.reason);
667
- }
668
- }
669
- export async function instructionsBlockCommand(options, io) {
670
- const result = await updateManagedInstructionFile({
671
- filePath: resolveWorkspaceWritePath(process.cwd(), requireArg(options, "file")),
672
- contentPath: resolveWorkspaceWritePath(process.cwd(), requireArg(options, "content-file")),
673
- input: {
674
- blockId: requireArg(options, "block"),
675
- generator: stringOption(options.generator) ?? "open-orchestra",
676
- version: stringOption(options.version) ?? "1",
677
- target: parseInstructionTarget(stringOption(options.target) ?? "generic"),
678
- sourceManifest: stringOption(options["source-manifest"]) ?? "local",
679
- },
680
- check: Boolean(options.check),
681
- dryRun: Boolean(options["dry-run"]),
682
- force: Boolean(options.force),
683
- });
684
- if (options.json) {
685
- io.log(JSON.stringify(result, null, 2));
686
- return;
687
- }
688
- if (result.drift) {
689
- io.log("Drift detected for " + requireArg(options, "block"));
690
- return;
691
- }
692
- io.log(result.changed
693
- ? "Instruction block changed"
694
- : "Instruction block unchanged");
695
- }
696
- export async function instructionsImportsCommand(options, io) {
697
- const result = await resolveInstructionImportsFromFile({
698
- registryPath: requireArg(options, "registry"),
699
- entryId: requireArg(options, "entry"),
700
- target: parseInstructionTarget(stringOption(options.target) ?? "generic"),
701
- });
702
- if (options.json) {
703
- io.log(JSON.stringify(result, null, 2));
704
- return;
705
- }
706
- io.log(result.content);
707
- }
708
- function parseInstructionTarget(value) {
709
- return parseRuntimeTarget(value);
710
- }
711
556
  export async function collaborationFlowsCommand(options, io) {
712
557
  const flows = listCollaborationFlows();
713
558
  if (options.json) {
@@ -788,283 +633,6 @@ export async function workflowTemplateRenderCommand(options, io) {
788
633
  }
789
634
  io.log(rendered.content);
790
635
  }
791
- export async function commandsManifestCommand(options, io) {
792
- const manifest = listCommandManifest();
793
- if (options.json) {
794
- io.log(JSON.stringify(manifest, null, 2));
795
- return;
796
- }
797
- for (const command of manifest) {
798
- io.log(command.command + " - " + command.summary);
799
- }
800
- }
801
- export async function runtimeAdaptersCommand(options, io) {
802
- const adapters = listRuntimeAdapters();
803
- if (options.json) {
804
- io.log(JSON.stringify(adapters, null, 2));
805
- return;
806
- }
807
- for (const adapter of adapters) {
808
- io.log(adapter.target +
809
- " - " +
810
- adapter.label +
811
- " (" +
812
- adapter.defaultInstructionFiles.join(", ") +
813
- ")");
814
- }
815
- }
816
- export async function runtimeBootstrapCommand(options, io) {
817
- const target = parseSkillRenderTarget(stringOption(options.target) ?? "generic");
818
- const bootstrap = renderRuntimeBootstrap(target);
819
- const filePath = stringOption(options.file);
820
- if (filePath) {
821
- const resolvedFilePath = resolveWorkspaceWritePath(process.cwd(), path.normalize(filePath));
822
- const existing = await readFile(resolvedFilePath, "utf8").catch(() => "");
823
- const result = upsertRuntimeBootstrapBlock(existing, bootstrap, {
824
- force: Boolean(options.force),
825
- });
826
- if (!options.check && !options["dry-run"] && !result.drift) {
827
- await writeFile(resolvedFilePath, result.content);
828
- }
829
- if (options.json) {
830
- io.log(JSON.stringify(result, null, 2));
831
- return;
832
- }
833
- io.log(result.changed
834
- ? "Runtime bootstrap changed"
835
- : "Runtime bootstrap unchanged");
836
- return;
837
- }
838
- if (options.json) {
839
- io.log(JSON.stringify(bootstrap, null, 2));
840
- return;
841
- }
842
- io.log(bootstrap.content);
843
- }
844
- export async function protocolRenderCommand(options, io) {
845
- const protocol = await renderSubagentProtocol(removeUndefined({
846
- target: parseSkillRenderTarget(stringOption(options.target) ?? "generic"),
847
- taskId: stringOption(options.task),
848
- }));
849
- if (options.json) {
850
- io.log(JSON.stringify(protocol, null, 2));
851
- return;
852
- }
853
- io.log(protocol.content);
854
- }
855
- export async function protocolBlockCommand(options, io) {
856
- const filePath = requireArg(options, "file");
857
- const protocol = await renderSubagentProtocol(removeUndefined({
858
- target: parseSkillRenderTarget(stringOption(options.target) ?? "generic"),
859
- taskId: stringOption(options.task),
860
- }));
861
- const existing = await readFile(filePath, "utf8").catch(() => "");
862
- const result = upsertSubagentProtocolBlock(existing, protocol, {
863
- force: Boolean(options.force),
864
- });
865
- const shouldWrite = !options.check && !options["dry-run"] && !result.drift && result.changed;
866
- if (shouldWrite) {
867
- await writeFile(filePath, result.content);
868
- }
869
- if (options.json) {
870
- io.log(JSON.stringify(result, null, 2));
871
- return;
872
- }
873
- if (result.drift) {
874
- io.log("Drift detected for subagent protocol block");
875
- return;
876
- }
877
- io.log(result.changed
878
- ? "Subagent protocol block changed"
879
- : "Subagent protocol block unchanged");
880
- }
881
- export async function diagramsLintCommand(options, io) {
882
- const result = await lintMermaidDiagram({
883
- filePath: requireArg(options, "file"),
884
- });
885
- const evidence = stringOption(options.task)
886
- ? await recordDiagramLintEvidence({
887
- taskId: stringOption(options.task) ?? "",
888
- result,
889
- })
890
- : undefined;
891
- const output = removeUndefined({ result, evidence });
892
- if (options.json) {
893
- io.log(JSON.stringify(output, null, 2));
894
- }
895
- else {
896
- io.log(result.valid ? "Mermaid diagram valid" : "Mermaid diagram invalid");
897
- if (result.installHint) {
898
- io.log(result.installHint);
899
- }
900
- if (evidence) {
901
- io.log("Created " + evidence.artifact);
902
- }
903
- }
904
- if (!result.valid) {
905
- throw new Error("diagram lint failed");
906
- }
907
- }
908
- export async function mcpOAuthProxyEvaluateCommand(options, io) {
909
- const evaluation = evaluateMcpOAuthProxy(removeUndefined({
910
- enabled: Boolean(options.enable),
911
- mode: parseMcpProxyMode(stringOption(options.mode) ?? "stdio-proxy"),
912
- serverUrl: requireArg(options, "server-url"),
913
- tokenStorage: parseMcpSecretStorage(stringOption(options["token-storage"]) ?? "keychain"),
914
- tokenPath: stringOption(options["token-path"]),
915
- refreshWindowSeconds: Number(stringOption(options["refresh-window"]) ?? 300),
916
- approvedBy: options.approve
917
- ? (stringOption(options.approver) ?? "local-user")
918
- : undefined,
919
- }));
920
- if (options.json) {
921
- io.log(JSON.stringify(evaluation, null, 2));
922
- return;
923
- }
924
- io.log(evaluation.approved
925
- ? "MCP proxy plan approved"
926
- : "MCP proxy plan has risks");
927
- for (const risk of evaluation.risks) {
928
- io.log("RISK: " + risk);
929
- }
930
- }
931
- function parseMcpProxyMode(value) {
932
- if (["stdio-proxy", "direct-http", "tool-native-oauth"].includes(value)) {
933
- return value;
934
- }
935
- throw new Error("unknown MCP proxy mode: " + value);
936
- }
937
- function parseMcpSecretStorage(value) {
938
- if (["keychain", "libsecret", "windows-credential", "secure-file"].includes(value)) {
939
- return value;
940
- }
941
- throw new Error("unknown MCP secret storage: " + value);
942
- }
943
- export async function skillsValidateCommand(options, io) {
944
- const report = await validateSkills();
945
- if (options.json) {
946
- io.log(JSON.stringify(report, null, 2));
947
- }
948
- else {
949
- io.log(report.valid ? "Skills valid" : "Skills invalid");
950
- for (const error of report.errors) {
951
- io.log(`ERROR: ${error}`);
952
- }
953
- for (const warning of report.warnings) {
954
- io.log(`WARN: ${warning}`);
955
- }
956
- }
957
- if (!report.valid) {
958
- throw new Error("skills validation failed");
959
- }
960
- }
961
- export async function sourcesListCommand(options, io) {
962
- const sources = await readSourceOfTruth();
963
- if (options.json) {
964
- io.log(JSON.stringify(sources, null, 2));
965
- return;
966
- }
967
- for (const source of sources) {
968
- io.log(`${source.id ?? "unknown"} - ${source.name ?? "Unnamed source"}`);
969
- io.log(` ${(source.locations ?? []).join(", ")}`);
970
- }
971
- }
972
- export async function lessonsListCommand(options, io) {
973
- const lessons = await listAgentLessons();
974
- if (options.json) {
975
- io.log(JSON.stringify(lessons, null, 2));
976
- return;
977
- }
978
- if (lessons.length === 0) {
979
- io.log("No lessons");
980
- return;
981
- }
982
- for (const lesson of lessons) {
983
- io.log(`${lesson.timestamp} ${lesson.operation}: ${lesson.errorSignature}`);
984
- }
985
- }
986
- export async function lessonsAddCommand(options, io) {
987
- const lesson = await addAgentLesson(removeUndefined({
988
- taskId: stringOption(options.task),
989
- actor: stringOption(options.actor) ?? "parent",
990
- operation: requireArg(options, "operation"),
991
- failedAction: requireArg(options, "failed-action"),
992
- errorSignature: requireArg(options, "error-signature"),
993
- rootCause: requireArg(options, "root-cause"),
994
- fix: requireArg(options, "fix"),
995
- prevention: requireArg(options, "prevention"),
996
- appliesTo: parseCsv(options["applies-to"]),
997
- verifiedBy: parseCsv(options["verified-by"]),
998
- }));
999
- if (options.json) {
1000
- io.log(JSON.stringify(lesson, null, 2));
1001
- return;
1002
- }
1003
- io.log(`Recorded lesson ${lesson.errorSignature}`);
1004
- }
1005
- export async function lessonsPromoteCommand(options, io) {
1006
- const result = await promoteAgentLessons(removeUndefined({
1007
- to: parsePromotionTarget(stringOption(options.to) ?? "doc"),
1008
- filter: stringOption(options.filter),
1009
- }));
1010
- if (options.json) {
1011
- io.log(JSON.stringify(result, null, 2));
1012
- return;
1013
- }
1014
- io.log(`Created ${result.artifact}`);
1015
- io.log(`Promoted lessons: ${result.lessons.length}`);
1016
- }
1017
- function parsePromotionTarget(value) {
1018
- if (["skill", "rule", "doc"].includes(value)) {
1019
- return value;
1020
- }
1021
- throw new Error(`unknown promotion target: ${value}`);
1022
- }
1023
- export async function skillsListCommand(options, io) {
1024
- const skills = listSkills();
1025
- if (options.json) {
1026
- io.log(JSON.stringify(skills, null, 2));
1027
- return;
1028
- }
1029
- for (const skill of skills) {
1030
- io.log(`${skill.id} - ${skill.name} (${skill.loadBudget})`);
1031
- io.log(` ${skill.summary}`);
1032
- io.log(` sources: ${skill.sourceGroups.join(", ")}`);
1033
- }
1034
- }
1035
- export async function skillsPlanCommand(options, io) {
1036
- const plan = await planSkillsForTask(requireArg(options, "task"));
1037
- await recordSkillPlan(plan);
1038
- if (options.json) {
1039
- io.log(JSON.stringify(plan, null, 2));
1040
- return;
1041
- }
1042
- io.log(`Skills for ${plan.taskId}:`);
1043
- for (const item of plan.selected) {
1044
- io.log(` ${item.skill.id} (score ${item.score})`);
1045
- for (const reason of item.rationale) {
1046
- io.log(` - ${reason}`);
1047
- }
1048
- }
1049
- io.log(`Source groups: ${plan.sourceGroups.join(", ")}`);
1050
- }
1051
- export async function skillsRenderCommand(options, io) {
1052
- const target = parseSkillRenderTarget(stringOption(options.target) ?? "generic");
1053
- const rendered = await renderSkills({
1054
- target,
1055
- taskId: stringOption(options.task),
1056
- skillIds: parseCsv(options.skills),
1057
- });
1058
- await recordSkillRender(rendered);
1059
- if (options.json) {
1060
- io.log(JSON.stringify(rendered, null, 2));
1061
- return;
1062
- }
1063
- io.log(rendered.content);
1064
- }
1065
- function parseSkillRenderTarget(value) {
1066
- return parseRuntimeTarget(value);
1067
- }
1068
636
  // --- Autonomous workflow commands ---
1069
637
  const GATE_MODES = ["none", "phase", "all"];
1070
638
  export async function workflowRunCommand(options, io) {
@@ -1072,12 +640,22 @@ export async function workflowRunCommand(options, io) {
1072
640
  const taskId = requireArg(options, "task");
1073
641
  const gates = (stringOption(options.gates) ?? "phase");
1074
642
  const maxIterations = numberOption(options["max-iterations"], 5);
643
+ const timeoutMinutes = numberOption(options["timeout-minutes"], 0);
1075
644
  const file = autonomousRunsPath(cwd);
645
+ const phaseSelection = await resolveWorkflowPhaseSelection(cwd, options);
646
+ const config = await getWorkflowConfig(cwd);
647
+ const phaseTimeoutMinutes = config.workflow?.phaseTimeoutMinutes ?? 0;
1076
648
  if (!GATE_MODES.includes(gates)) {
1077
649
  throw new Error(`--gates must be one of: ${GATE_MODES.join(", ")}`);
1078
650
  }
651
+ if (timeoutMinutes < 0) {
652
+ throw new Error("--timeout-minutes must be 0 or greater");
653
+ }
654
+ if (phaseTimeoutMinutes < 0) {
655
+ throw new Error("workflow.phaseTimeoutMinutes must be 0 or greater");
656
+ }
1079
657
  if (options["dry-run"]) {
1080
- return workflowDryRun(options, io, taskId, gates, maxIterations);
658
+ return workflowDryRun(options, io, taskId, gates, maxIterations, phaseSelection);
1081
659
  }
1082
660
  let run;
1083
661
  let startIndex;
@@ -1086,21 +664,39 @@ export async function workflowRunCommand(options, io) {
1086
664
  if (!existing)
1087
665
  throw new Error(`workflow run not found: ${String(options.resume)}`);
1088
666
  run = existing;
1089
- startIndex = resumePhaseIndex(run);
667
+ if (run.status === "canceled" || run.status === "failed") {
668
+ throw new Error(`workflow run ${run.id} is ${run.status} and cannot be resumed`);
669
+ }
670
+ const resumeSequence = phaseSequenceForRun(run, phaseSelection.sequence);
671
+ startIndex = resumePhaseIndex(run, resumeSequence);
1090
672
  if (startIndex === -1) {
1091
673
  io.log(`Workflow ${run.id} is already complete`);
1092
674
  if (options.json)
1093
675
  io.log(JSON.stringify({ run, file, cwd }, null, 2));
1094
676
  return;
1095
677
  }
1096
- io.log(`Resuming run ${run.id} from phase ${AUTONOMOUS_PHASE_SEQUENCE[startIndex]?.phase}`);
678
+ const pausedGate = run.phases.findLast((phase) => phase.status === "gate_paused");
679
+ if (pausedGate && !pausedGate.approvedBy) {
680
+ io.log(`WARN: gate ${pausedGate.gateId ?? gateIdForPausedPhase(pausedGate.phase, resumeSequence)} has no recorded approval; continuing for backward compatibility`);
681
+ }
682
+ io.log(`Resuming run ${run.id} from phase ${resumeSequence[startIndex]?.phase}`);
683
+ run = { ...run, phaseSequence: resumeSequence.map((phase) => phase.phase) };
1097
684
  }
1098
685
  else {
1099
- run = await createAutonomousRun(cwd, { taskId, gates, maxIterations });
686
+ run = await createAutonomousRun(cwd, {
687
+ taskId,
688
+ gates,
689
+ maxIterations,
690
+ phaseSequence: phaseSelection.sequence.map((phase) => phase.phase),
691
+ skippedPhases: phaseSelection.skipped,
692
+ });
1100
693
  startIndex = 0;
1101
694
  io.log(`Started autonomous workflow ${run.id} for task ${taskId} [gates=${gates}]`);
1102
695
  }
1103
- run = await executePhases(cwd, run, startIndex, io);
696
+ run = await executePhases(cwd, run, startIndex, io, phaseSequenceForRun(run, phaseSelection.sequence), {
697
+ runTimeoutMinutes: timeoutMinutes,
698
+ phaseTimeoutMinutes,
699
+ });
1104
700
  const result = { run, file, cwd };
1105
701
  if (options.json) {
1106
702
  io.log(JSON.stringify(result, null, 2));
@@ -1118,7 +714,9 @@ export async function workflowRunCommand(options, io) {
1118
714
  }
1119
715
  export async function workflowRunListCommand(options, io) {
1120
716
  const cwd = process.cwd();
1121
- const runs = await listAutonomousRuns(cwd);
717
+ const runs = options.active
718
+ ? await listActiveAutonomousRuns(cwd)
719
+ : await listAutonomousRuns(cwd);
1122
720
  if (options.json) {
1123
721
  io.log(JSON.stringify(runs, null, 2));
1124
722
  return;
@@ -1134,17 +732,35 @@ export async function workflowRunListCommand(options, io) {
1134
732
  io.log(` ${phases}`);
1135
733
  }
1136
734
  }
1137
- const CLARIFICATION_TARGETS = ["po", "architect"];
1138
- const CLARIFICATION_ALLOWED_PHASES = new Set(["developer", "qa"]);
735
+ export async function workflowCancelCommand(options, io) {
736
+ const run = await cancelRun(process.cwd(), requireArg(options, "run"), stringOption(options.reason) ?? "Workflow run canceled");
737
+ if (options.json) {
738
+ io.log(JSON.stringify(run, null, 2));
739
+ return;
740
+ }
741
+ io.log(`Workflow canceled [run=${run.id}]`);
742
+ }
743
+ export async function workflowGateApproveCommand(options, io) {
744
+ const approval = await approveWorkflowGate({
745
+ runId: requireArg(options, "run"),
746
+ gateId: requireArg(options, "gate"),
747
+ approver: requireArg(options, "approver"),
748
+ rationale: requireArg(options, "rationale"),
749
+ });
750
+ if (options.json) {
751
+ io.log(JSON.stringify(approval, null, 2));
752
+ return;
753
+ }
754
+ io.log(approval.alreadyApproved
755
+ ? `Gate ${approval.gateId} already approved by ${approval.approver}`
756
+ : `Approved gate ${approval.gateId} for run ${approval.run.id}`);
757
+ }
1139
758
  export async function workflowClarifyCommand(options, io) {
1140
759
  const cwd = process.cwd();
1141
760
  const runId = requireArg(options, "run");
1142
- const from = requireArg(options, "from");
1143
- const to = requireArg(options, "to");
761
+ const from = await resolveClarificationRole(cwd, requireArg(options, "from"));
762
+ const to = await resolveClarificationRole(cwd, requireArg(options, "to"));
1144
763
  const question = requireArg(options, "question");
1145
- if (!CLARIFICATION_TARGETS.includes(to)) {
1146
- throw new Error(`--to must be one of: ${CLARIFICATION_TARGETS.join(", ")}`);
1147
- }
1148
764
  const run = await readAutonomousRun(cwd, runId);
1149
765
  if (!run)
1150
766
  throw new Error(`workflow run not found: ${runId}`);
@@ -1155,9 +771,6 @@ export async function workflowClarifyCommand(options, io) {
1155
771
  throw new Error(`no active phase found for role ${from} in run ${runId}`);
1156
772
  }
1157
773
  const activePhase = run.phases[activePhaseIdx];
1158
- if (!CLARIFICATION_ALLOWED_PHASES.has(activePhase.phase)) {
1159
- throw new Error(`clarification is only allowed from developer or qa phases (active: ${activePhase.phase})`);
1160
- }
1161
774
  // Open clarification record
1162
775
  const record = await openClarification(cwd, {
1163
776
  runId,
@@ -1168,8 +781,9 @@ export async function workflowClarifyCommand(options, io) {
1168
781
  });
1169
782
  // Suspend the phase if it's still running
1170
783
  if (activePhase.status === "running") {
1171
- const phaseSeqIdx = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.phase === activePhase.phase);
1172
- await suspendPhaseForClarification(cwd, run, phaseSeqIdx);
784
+ const sequence = phaseSequenceForRun(run);
785
+ const phaseSeqIdx = sequence.findIndex((d) => d.phase === activePhase.phase);
786
+ await suspendPhaseForClarification(cwd, run, phaseSeqIdx, sequence);
1173
787
  }
1174
788
  if (options.json) {
1175
789
  io.log(JSON.stringify(record, null, 2));
@@ -1193,11 +807,12 @@ export async function workflowClarifyRespondCommand(options, io) {
1193
807
  answer,
1194
808
  });
1195
809
  // Resume the suspended phase back to running
1196
- const phaseSeqIdx = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.role === record.fromRole);
810
+ const sequence = phaseSequenceForRun(run);
811
+ const phaseSeqIdx = sequence.findIndex((d) => d.role === record.fromRole);
1197
812
  if (phaseSeqIdx !== -1) {
1198
813
  const phase = run.phases.find((p) => p.role === record.fromRole);
1199
814
  if (phase?.status === "awaiting_clarification") {
1200
- await resumePhaseFromClarification(cwd, run, phaseSeqIdx);
815
+ await resumePhaseFromClarification(cwd, run, phaseSeqIdx, sequence);
1201
816
  }
1202
817
  }
1203
818
  if (options.json) {
@@ -1211,14 +826,25 @@ export async function workflowClarifyRespondCommand(options, io) {
1211
826
  }
1212
827
  export async function workflowClarifyListCommand(options, io) {
1213
828
  const cwd = process.cwd();
1214
- const runId = requireArg(options, "run");
1215
- const clarifications = await listClarifications(cwd, runId);
829
+ const runId = stringOption(options.run);
830
+ const taskId = stringOption(options.task);
831
+ if (runId && taskId) {
832
+ throw new Error("workflow clarify-list accepts either --run or --task, not both");
833
+ }
834
+ if (!runId && !taskId) {
835
+ throw new Error("workflow clarify-list requires --run or --task");
836
+ }
837
+ const clarifications = runId
838
+ ? await listClarifications(cwd, runId)
839
+ : await listClarificationsByTask(cwd, taskId);
1216
840
  if (options.json) {
1217
841
  io.log(JSON.stringify(clarifications, null, 2));
1218
842
  return;
1219
843
  }
1220
844
  if (clarifications.length === 0) {
1221
- io.log(`No clarifications for run ${runId}`);
845
+ io.log(runId
846
+ ? `No clarifications for run ${runId}`
847
+ : `No clarifications for task ${taskId}`);
1222
848
  return;
1223
849
  }
1224
850
  for (const c of clarifications) {
@@ -1229,17 +855,115 @@ export async function workflowClarifyListCommand(options, io) {
1229
855
  io.log(` A: ${c.answer}`);
1230
856
  }
1231
857
  }
1232
- const DEV_PHASE_INDEX = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.phase === "developer");
1233
- async function executePhases(cwd, run, startIndex, io) {
858
+ const MEMORY_HOOK_POINTS = [
859
+ "before_plan",
860
+ "before_implementation",
861
+ "before_handoff",
862
+ "before_final",
863
+ "after_failure",
864
+ ];
865
+ function parseMemoryHook(value) {
866
+ const hook = value ?? "before_implementation";
867
+ if (!MEMORY_HOOK_POINTS.includes(hook)) {
868
+ throw new Error(`memory hook must be one of: ${MEMORY_HOOK_POINTS.join(", ")}`);
869
+ }
870
+ return hook;
871
+ }
872
+ async function resolveWorkflowPhaseSelection(root, options) {
873
+ const config = await getWorkflowConfig(root);
874
+ const configuredIds = config.workflow?.phaseSequence;
875
+ const baseSequence = configuredIds
876
+ ? phaseSequenceFromIds(configuredIds, "workflow.phaseSequence")
877
+ : AUTONOMOUS_PHASE_SEQUENCE;
878
+ const skipIds = parseCsv(options.skip);
879
+ validatePhaseIds(skipIds, "--skip");
880
+ const skipSet = new Set(skipIds);
881
+ const sequence = baseSequence.filter((phase) => !skipSet.has(phase.phase));
882
+ validateWorkflowPhaseSequence(sequence);
883
+ const sequenceIds = new Set(sequence.map((phase) => phase.phase));
884
+ const skipped = AUTONOMOUS_PHASE_SEQUENCE.filter((phase) => !sequenceIds.has(phase.phase));
885
+ return { sequence, skipped };
886
+ }
887
+ function phaseSequenceForRun(run, fallback = AUTONOMOUS_PHASE_SEQUENCE) {
888
+ return run.phaseSequence
889
+ ? phaseSequenceFromIds(run.phaseSequence, "run.phaseSequence")
890
+ : fallback;
891
+ }
892
+ function phaseSequenceFromIds(ids, source) {
893
+ validatePhaseIds(ids, source);
894
+ const byId = new Map(AUTONOMOUS_PHASE_SEQUENCE.map((phase) => [phase.phase, phase]));
895
+ return ids.map((id) => byId.get(id));
896
+ }
897
+ function validatePhaseIds(ids, source) {
898
+ const valid = new Set(AUTONOMOUS_PHASE_SEQUENCE.map((phase) => phase.phase));
899
+ const unknown = ids.filter((id) => !valid.has(id));
900
+ if (unknown.length > 0) {
901
+ throw new Error(`${source} contains invalid phase(s): ${unknown.join(", ")}. Valid phases: ${[...valid].join(", ")}`);
902
+ }
903
+ }
904
+ function validateWorkflowPhaseSequence(sequence) {
905
+ if (sequence.length === 0) {
906
+ throw new Error("workflow phase sequence cannot be empty");
907
+ }
908
+ const ids = sequence.map((phase) => phase.phase);
909
+ const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
910
+ if (duplicates.length > 0) {
911
+ throw new Error(`workflow phase sequence contains duplicate phase(s): ${[...new Set(duplicates)].join(", ")}`);
912
+ }
913
+ const qaIndex = ids.indexOf("qa");
914
+ const developerIndex = ids.indexOf("developer");
915
+ if (qaIndex !== -1 && developerIndex === -1) {
916
+ throw new Error("workflow phase sequence with qa must include developer");
917
+ }
918
+ if (qaIndex !== -1 && developerIndex > qaIndex) {
919
+ throw new Error("workflow phase sequence must place developer before qa");
920
+ }
921
+ }
922
+ function phaseExceededTimeout(run, phaseId, timeoutMs) {
923
+ const phase = run.phases.find((candidate) => candidate.phase === phaseId &&
924
+ (candidate.status === "running" ||
925
+ candidate.status === "awaiting_clarification"));
926
+ if (!phase) {
927
+ return false;
928
+ }
929
+ return Date.now() - new Date(phase.startedAt).getTime() > timeoutMs;
930
+ }
931
+ async function recordWorkflowTimeout(root, run, input) {
932
+ await appendEvent(root, {
933
+ type: "WORKFLOW_TIMEOUT",
934
+ taskId: run.taskId,
935
+ actor: "parent",
936
+ summary: input.reason,
937
+ metadata: {
938
+ runId: run.id,
939
+ phase: input.phase,
940
+ timeoutMinutes: input.timeoutMinutes,
941
+ },
942
+ });
943
+ }
944
+ function gateIdForPausedPhase(phase, sequence = AUTONOMOUS_PHASE_SEQUENCE) {
945
+ const sequenceIndex = sequence.findIndex((candidate) => candidate.phase === phase);
946
+ const next = sequence[sequenceIndex + 1]?.phase ?? "end";
947
+ return `${phase}->${next}`;
948
+ }
949
+ async function executePhases(cwd, run, startIndex, io, sequence, timeoutOptions) {
1234
950
  let current = run;
1235
951
  let qaFailNotes;
1236
- for (let i = startIndex; i < AUTONOMOUS_PHASE_SEQUENCE.length; i++) {
1237
- const def = AUTONOMOUS_PHASE_SEQUENCE[i];
952
+ const devPhaseIndex = sequence.findIndex((d) => d.phase === "developer");
953
+ const runDeadlineMs = timeoutOptions.runTimeoutMinutes > 0
954
+ ? new Date(run.createdAt).getTime() +
955
+ timeoutOptions.runTimeoutMinutes * 60 * 1000
956
+ : undefined;
957
+ const phaseTimeoutMs = timeoutOptions.phaseTimeoutMinutes > 0
958
+ ? timeoutOptions.phaseTimeoutMinutes * 60 * 1000
959
+ : undefined;
960
+ for (let i = startIndex; i < sequence.length; i++) {
961
+ const def = sequence[i];
1238
962
  // Init phase task (skip if already has a running/done/clarification record from resume)
1239
963
  const existing = current.phases.find((p) => p.phase === def.phase && p.status !== "qa_failed");
1240
964
  if (!existing || existing.status === "pending") {
1241
965
  const retryContext = def.phase === "developer" && qaFailNotes ? qaFailNotes : undefined;
1242
- const phaseRecord = await initPhase(cwd, current, i, retryContext);
966
+ const phaseRecord = await initPhase(cwd, current, i, retryContext, sequence);
1243
967
  current = { ...current, phases: [...current.phases, phaseRecord] };
1244
968
  if (retryContext) {
1245
969
  io.log(`↺ ${def.phase} (${def.role}) retry — QA findings: ${retryContext.slice(0, 80)}`);
@@ -1265,7 +989,7 @@ async function executePhases(cwd, run, startIndex, io) {
1265
989
  }).catch(async (error) => {
1266
990
  const message = error instanceof Error ? error.message : String(error);
1267
991
  io.log(`✗ ${def.phase} provider execution failed: ${message}`);
1268
- current = await markRunFailed(cwd, current, message);
992
+ current = await markRunFailed(cwd, current, message, def.phase);
1269
993
  return null;
1270
994
  });
1271
995
  if (!llmResult)
@@ -1273,6 +997,17 @@ async function executePhases(cwd, run, startIndex, io) {
1273
997
  if (llmResult.mode === "llm") {
1274
998
  io.log(` ✓ llm artifact (${llmResult.artifact})`);
1275
999
  }
1000
+ if (phaseTimeoutMs !== undefined &&
1001
+ phaseExceededTimeout(current, def.phase, phaseTimeoutMs)) {
1002
+ io.log(`✗ ${def.phase} phase timeout`);
1003
+ await recordWorkflowTimeout(cwd, current, {
1004
+ phase: def.phase,
1005
+ reason: "phase timeout",
1006
+ timeoutMinutes: timeoutOptions.phaseTimeoutMinutes,
1007
+ });
1008
+ current = await markRunFailed(cwd, current, "phase timeout", def.phase);
1009
+ return current;
1010
+ }
1276
1011
  // Architect sizing gate — always enforced regardless of --gates mode.
1277
1012
  // In LLM mode, the architect phase records the sizing decision before this check.
1278
1013
  if (def.phase === "architect") {
@@ -1288,15 +1023,15 @@ async function executePhases(cwd, run, startIndex, io) {
1288
1023
  io.log(` ✓ sizing=${sizing.sizing}${sizing.points !== undefined ? ` (${sizing.points} pts)` : ""}`);
1289
1024
  }
1290
1025
  const outcome = llmResult.outcome;
1291
- const result = await closePhase(cwd, current, i, outcome);
1026
+ const result = await closePhase(cwd, current, i, outcome, sequence);
1292
1027
  current = result.run;
1293
1028
  if (result.handoffArtifact) {
1294
- io.log(` ✓ handoff → ${AUTONOMOUS_PHASE_SEQUENCE[i + 1]?.role ?? "end"} (${result.handoffArtifact})`);
1029
+ io.log(` ✓ handoff → ${sequence[i + 1]?.role ?? "end"} (${result.handoffArtifact})`);
1295
1030
  }
1296
1031
  if (result.reviewArtifact) {
1297
- const nextPhase = AUTONOMOUS_PHASE_SEQUENCE[i + 1]?.phase;
1032
+ const nextPhase = sequence[i + 1]?.phase;
1298
1033
  io.log(` ⏸ gate ${def.phase}→${nextPhase} — review: ${result.reviewArtifact}`);
1299
- io.log(` Approve: orchestra workflow run --task ${current.taskId} --resume ${current.id}`);
1034
+ io.log(` Approve: orchestra workflow gate-approve --run ${current.id} --gate ${def.phase}->${nextPhase} --approver <name> --rationale "<text>"`);
1300
1035
  io.log(``);
1301
1036
  io.log(`╔══ GATE PAUSE: ${def.phase.toUpperCase()} → ${nextPhase?.toUpperCase()} ══════════════════════`);
1302
1037
  io.log(`║ Run: ${current.id}`);
@@ -1304,6 +1039,8 @@ async function executePhases(cwd, run, startIndex, io) {
1304
1039
  io.log(`║ Review: ${result.reviewArtifact}`);
1305
1040
  io.log(`║`);
1306
1041
  io.log(`║ To approve, run:`);
1042
+ io.log(`║ orchestra workflow gate-approve --run ${current.id} --gate ${def.phase}->${nextPhase} --approver <name> --rationale "<text>"`);
1043
+ io.log(`║ Then resume:`);
1307
1044
  io.log(`║ orchestra workflow run --task ${current.taskId} --resume ${current.id}`);
1308
1045
  io.log(`╚══════════════════════════════════════════════════════════════`);
1309
1046
  return current;
@@ -1313,7 +1050,7 @@ async function executePhases(cwd, run, startIndex, io) {
1313
1050
  if (closedPhase) {
1314
1051
  qaFailNotes = closedPhase.notes ?? "QA findings — see QA phase artifact";
1315
1052
  io.log(` ✗ qa failed (iteration ${current.qaIterations}/${current.maxIterations}) — routing back to developer`);
1316
- i = DEV_PHASE_INDEX - 1; // will be incremented to DEV_PHASE_INDEX at top of loop
1053
+ i = devPhaseIndex - 1; // will be incremented to developer at top of loop
1317
1054
  continue;
1318
1055
  }
1319
1056
  // Release phase: auto-create PR if configured
@@ -1344,18 +1081,29 @@ async function executePhases(cwd, run, startIndex, io) {
1344
1081
  }
1345
1082
  }
1346
1083
  }
1084
+ if (runDeadlineMs !== undefined && Date.now() > runDeadlineMs) {
1085
+ const reason = `wall-clock timeout after ${timeoutOptions.runTimeoutMinutes} minutes`;
1086
+ io.log(`✗ ${reason}`);
1087
+ await recordWorkflowTimeout(cwd, current, {
1088
+ phase: def.phase,
1089
+ reason,
1090
+ timeoutMinutes: timeoutOptions.runTimeoutMinutes,
1091
+ });
1092
+ current = await markRunFailed(cwd, current, reason);
1093
+ return current;
1094
+ }
1347
1095
  }
1348
1096
  current = await markRunDone(cwd, current);
1349
1097
  return current;
1350
1098
  }
1351
- async function workflowDryRun(options, io, taskId, gates, maxIterations) {
1099
+ async function workflowDryRun(options, io, taskId, gates, maxIterations, phaseSelection) {
1352
1100
  const gateTransitions = new Set(["po→architect", "qa→release"]);
1353
1101
  io.log(`Dry run — no records will be created`);
1354
1102
  io.log(`Task: ${taskId} gates: ${gates} max-iterations: ${maxIterations}`);
1355
1103
  io.log(``);
1356
- for (let i = 0; i < AUTONOMOUS_PHASE_SEQUENCE.length; i++) {
1357
- const def = AUTONOMOUS_PHASE_SEQUENCE[i];
1358
- const next = AUTONOMOUS_PHASE_SEQUENCE[i + 1];
1104
+ for (let i = 0; i < phaseSelection.sequence.length; i++) {
1105
+ const def = phaseSelection.sequence[i];
1106
+ const next = phaseSelection.sequence[i + 1];
1359
1107
  const transitionKey = next ? `${def.phase}→${next.phase}` : "";
1360
1108
  const gateLabel = gates === "all"
1361
1109
  ? "gate=yes"
@@ -1364,235 +1112,21 @@ async function workflowDryRun(options, io, taskId, gates, maxIterations) {
1364
1112
  : "gate=no";
1365
1113
  io.log(` ${def.phase} (${def.role}) ${gateLabel}`);
1366
1114
  }
1115
+ if (phaseSelection.skipped.length > 0) {
1116
+ io.log(``);
1117
+ io.log(`Skipped: ${phaseSelection.skipped.map((phase) => phase.phase).join(", ")}`);
1118
+ }
1367
1119
  if (options.json) {
1368
1120
  io.log(JSON.stringify({
1369
1121
  dryRun: true,
1370
1122
  taskId,
1371
1123
  gates,
1372
1124
  maxIterations,
1373
- phases: AUTONOMOUS_PHASE_SEQUENCE,
1125
+ phases: phaseSelection.sequence,
1126
+ skipped: phaseSelection.skipped,
1374
1127
  }, null, 2));
1375
1128
  }
1376
1129
  }
1377
- // --- Estimate & Benchmark commands ---
1378
- const CONFIDENCE_LEVELS = ["low", "medium", "high"];
1379
- export async function estimateCommand(options, io) {
1380
- const cwd = process.cwd();
1381
- const taskId = requireArg(options, "task");
1382
- const sizingRaw = requireArg(options, "sizing");
1383
- const soloDays = numberOption(options["solo-days"], 0);
1384
- const aiDays = numberOption(options["ai-unguided-days"], 0);
1385
- const confidence = (stringOption(options.confidence) ??
1386
- "medium");
1387
- const declaredBy = stringOption(options["declared-by"]) ?? "pm";
1388
- if (!SIZING_LABELS.includes(sizingRaw)) {
1389
- throw new Error(`--sizing must be one of: ${SIZING_LABELS.join(", ")}`);
1390
- }
1391
- if (!CONFIDENCE_LEVELS.includes(confidence)) {
1392
- throw new Error(`--confidence must be one of: ${CONFIDENCE_LEVELS.join(", ")}`);
1393
- }
1394
- if (soloDays <= 0)
1395
- throw new Error(`--solo-days must be > 0`);
1396
- if (aiDays <= 0)
1397
- throw new Error(`--ai-unguided-days must be > 0`);
1398
- const record = await recordEstimate(cwd, {
1399
- taskId,
1400
- sizingLabel: sizingRaw,
1401
- soloEstimateDays: soloDays,
1402
- aiUnguidedEstimateDays: aiDays,
1403
- confidence,
1404
- declaredBy,
1405
- });
1406
- if (options.json) {
1407
- io.log(JSON.stringify(record, null, 2));
1408
- return;
1409
- }
1410
- io.log(`Estimate recorded for ${taskId} [${record.id}]`);
1411
- io.log(` Sizing: ${record.sizingLabel}`);
1412
- io.log(` Solo: ${record.soloEstimateDays}d`);
1413
- io.log(` AI-unguided: ${record.aiUnguidedEstimateDays}d`);
1414
- io.log(` Confidence: ${record.confidence}`);
1415
- io.log(` Declared by: ${record.declaredBy}`);
1416
- }
1417
- export async function benchmarkCommand(options, io) {
1418
- const cwd = process.cwd();
1419
- if (options.summary) {
1420
- const summary = await summarizeBenchmark(cwd);
1421
- if (options.json) {
1422
- io.log(JSON.stringify(summary, null, 2));
1423
- return;
1424
- }
1425
- if (summary.stories.length === 0) {
1426
- io.log("No estimates recorded yet. Use: orchestra estimate --task <id> ...");
1427
- return;
1428
- }
1429
- const header = `${"Story".padEnd(14)} ${"Size".padEnd(4)} ${"Solo".padEnd(6)} ${"AI".padEnd(6)} ${"Actual".padEnd(8)} ${"vs Solo".padEnd(8)} ${"vs AI".padEnd(8)} ${"QA".padEnd(4)} ${"Rev".padEnd(4)} ${"Blk".padEnd(4)} ${"Ev".padEnd(4)} ${"Les"}`;
1430
- io.log(header);
1431
- io.log("─".repeat(header.length));
1432
- for (const s of summary.stories) {
1433
- const actual = s.actualDays !== null ? `${s.actualDays}d` : "pending";
1434
- const vsSolo = s.vsSoloPct !== null
1435
- ? `${s.vsSoloPct > 0 ? "+" : ""}${s.vsSoloPct}%`
1436
- : "—";
1437
- const vsAi = s.vsAiUnguidedPct !== null
1438
- ? `${s.vsAiUnguidedPct > 0 ? "+" : ""}${s.vsAiUnguidedPct}%`
1439
- : "—";
1440
- io.log(`${s.taskId.padEnd(14)} ${s.sizingLabel.padEnd(4)} ${`${s.soloEstimateDays}d`.padEnd(6)} ${`${s.aiUnguidedEstimateDays}d`.padEnd(6)} ${actual.padEnd(8)} ${vsSolo.padEnd(8)} ${vsAi.padEnd(8)} ${String(s.qaIterations).padEnd(4)} ${String(s.quality.reviewCount).padEnd(4)} ${String(s.quality.blockingReviews).padEnd(4)} ${String(s.quality.evidenceCount).padEnd(4)} ${s.quality.lessonCount}`);
1441
- }
1442
- if (summary.totalWithActuals > 0) {
1443
- io.log("");
1444
- io.log(`Avg savings vs solo: ${summary.avgVsSoloPct}%`);
1445
- io.log(`Avg savings vs AI-unguided: ${summary.avgVsAiUnguidedPct}%`);
1446
- io.log(`Stories with actuals: ${summary.totalWithActuals}/${summary.stories.length}`);
1447
- }
1448
- return;
1449
- }
1450
- const taskId = requireArg(options, "task");
1451
- const result = await computeBenchmark(cwd, taskId);
1452
- if (options.json) {
1453
- io.log(JSON.stringify(result, null, 2));
1454
- return;
1455
- }
1456
- io.log(`Benchmark: ${taskId} [${result.status}]`);
1457
- io.log(` Sizing: ${result.sizingLabel}`);
1458
- io.log(` Solo: ${result.soloEstimateDays}d (declared)`);
1459
- io.log(` AI-unguided: ${result.aiUnguidedEstimateDays}d (declared)`);
1460
- io.log(` Actual: ${result.actualDays !== null ? `${result.actualDays}d` : "pending — run not complete"}`);
1461
- if (result.vsSoloPct !== null) {
1462
- const sign = result.vsSoloPct > 0 ? "+" : "";
1463
- io.log(` vs Solo: ${sign}${result.vsSoloPct}%`);
1464
- io.log(` vs AI: ${result.vsAiUnguidedPct !== null ? `${result.vsAiUnguidedPct > 0 ? "+" : ""}${result.vsAiUnguidedPct}%` : "—"}`);
1465
- }
1466
- io.log(` QA loops: ${result.qaIterations}`);
1467
- io.log(` Reviews: ${result.quality.reviewCount} (${result.quality.blockingReviews} blocking)`);
1468
- io.log(` Evidence: ${result.quality.evidenceCount} artifacts`);
1469
- io.log(` Gate blocks: ${result.quality.gateBlockCount}`);
1470
- io.log(` Lessons: ${result.quality.lessonCount}`);
1471
- if (result.quality.totalInputTokens > 0 ||
1472
- result.quality.totalOutputTokens > 0) {
1473
- io.log(` Tokens: ${result.quality.totalInputTokens}in / ${result.quality.totalOutputTokens}out`);
1474
- io.log(` Cost: $${result.quality.estimatedCostUsd}`);
1475
- }
1476
- }
1477
- // --- Velocity command ---
1478
- export async function velocityCommand(options, io) {
1479
- const cwd = process.cwd();
1480
- if (typeof options.sprints === "string") {
1481
- const limit = Number(options.sprints);
1482
- if (!Number.isInteger(limit) || limit <= 0) {
1483
- throw new Error("--sprints must be a positive integer");
1484
- }
1485
- const report = await velocityTrend(cwd, limit);
1486
- if (options.json) {
1487
- io.log(JSON.stringify(report, null, 2));
1488
- return;
1489
- }
1490
- io.log(`Sprint Velocity Report`);
1491
- io.log(` Trend: ${report.trend}`);
1492
- for (const sprint of report.sprints) {
1493
- io.log(` ${sprint.sprintId} ${sprint.storiesCompleted} stories ${sprint.pointsCompleted} pts`);
1494
- }
1495
- return;
1496
- }
1497
- const report = await computeVelocity(cwd);
1498
- if (options.json) {
1499
- io.log(JSON.stringify(report, null, 2));
1500
- return;
1501
- }
1502
- io.log(`Velocity Report`);
1503
- io.log(` Stories completed: ${report.totalStoriesCompleted}`);
1504
- io.log(` Points completed: ${report.totalPointsCompleted}`);
1505
- io.log(` Avg actual days: ${report.avgActualDaysPerStory !== null ? report.avgActualDaysPerStory + "d" : "n/a"}`);
1506
- io.log(` vs Solo estimate: ${report.avgVsSoloPct !== null ? report.avgVsSoloPct + "%" : "n/a"}`);
1507
- io.log(` vs AI estimate: ${report.avgVsAiUnguidedPct !== null ? report.avgVsAiUnguidedPct + "%" : "n/a"}`);
1508
- io.log(` Trend: ${report.trend}`);
1509
- if (report.weeks.length > 0) {
1510
- io.log(`\nWeekly breakdown:`);
1511
- for (const week of report.weeks) {
1512
- io.log(` ${week.week} ${week.storiesCompleted} stories ${week.pointsCompleted} pts`);
1513
- }
1514
- }
1515
- }
1516
- // --- Sprint command ---
1517
- export async function sprintCommand(subcommand, options, io) {
1518
- const cwd = process.cwd();
1519
- if (subcommand === "start") {
1520
- const id = requireArg(options, "id");
1521
- const taskIds = requireArg(options, "tasks")
1522
- .split(",")
1523
- .map((taskId) => taskId.trim())
1524
- .filter(Boolean);
1525
- const sprint = await startSprint(cwd, id, taskIds);
1526
- if (options.json) {
1527
- io.log(JSON.stringify(sprint, null, 2));
1528
- return;
1529
- }
1530
- io.log(`Sprint started: ${sprint.id} (${sprint.taskIds.length} tasks)`);
1531
- return;
1532
- }
1533
- if (subcommand === "close") {
1534
- const id = requireArg(options, "id");
1535
- const sprint = await closeSprint(cwd, id);
1536
- if (options.json) {
1537
- io.log(JSON.stringify(sprint, null, 2));
1538
- return;
1539
- }
1540
- io.log(`Sprint closed: ${sprint.id}`);
1541
- return;
1542
- }
1543
- throw new Error(`unknown sprint command: ${subcommand ?? ""}`.trim());
1544
- }
1545
- // --- Calibration command ---
1546
- export async function calibrationCommand(options, io) {
1547
- const report = await calibrationReport(process.cwd());
1548
- const roleFilter = typeof options.role === "string" ? options.role : null;
1549
- const filtered = roleFilter
1550
- ? { roles: report.roles.filter((role) => role.role === roleFilter) }
1551
- : report;
1552
- if (options.json) {
1553
- io.log(JSON.stringify(filtered, null, 2));
1554
- return;
1555
- }
1556
- io.log(`Calibration Report`);
1557
- for (const role of filtered.roles) {
1558
- io.log(` ${role.role} stories=${role.stories} declared=${role.avgDeclaredDays}d actual=${role.avgActualDays}d error=${role.avgErrorPct}% bias=${role.bias}`);
1559
- }
1560
- }
1561
- // --- Burndown command ---
1562
- export async function burndownCommand(options, io) {
1563
- const cwd = process.cwd();
1564
- const sprintCsv = requireArg(options, "sprint");
1565
- const sprintTaskIds = sprintCsv
1566
- .split(",")
1567
- .map((s) => s.trim())
1568
- .filter(Boolean);
1569
- if (sprintTaskIds.length === 0)
1570
- throw new Error(`--sprint requires at least one task id`);
1571
- const series = await computeSprintBurndown(cwd, sprintTaskIds);
1572
- if (options.json) {
1573
- io.log(JSON.stringify(series, null, 2));
1574
- return;
1575
- }
1576
- for (const w of series.warnings)
1577
- io.log(`⚠ ${w}`);
1578
- if (series.totalPoints === 0) {
1579
- io.log("No points to chart — record estimates first with: orchestra estimate --task <id> ...");
1580
- return;
1581
- }
1582
- io.log(`Sprint burndown total=${series.totalPoints} pts tasks=${series.sprintTaskIds.length}`);
1583
- io.log("");
1584
- io.log(renderBurndownAscii(series));
1585
- io.log("");
1586
- io.log("Task breakdown:");
1587
- for (const t of series.taskBreakdown) {
1588
- const arch = t.architectPoints !== null ? `arch=${t.architectPoints}` : "arch=—";
1589
- const dev = t.developerPoints !== null ? `dev=${t.developerPoints}` : "dev=—";
1590
- const done = t.completedAt
1591
- ? `done ${t.completedAt.slice(0, 10)}`
1592
- : "pending";
1593
- io.log(` ${t.taskId.padEnd(14)} ${arch.padEnd(10)} ${dev.padEnd(10)} resolved=${t.resolvedPoints} ${done}`);
1594
- }
1595
- }
1596
1130
  function parseRuntimeTargetOptions(options) {
1597
1131
  const targetValues = [
1598
1132
  ...parseCsv(options.target),
@@ -1600,6 +1134,9 @@ function parseRuntimeTargetOptions(options) {
1600
1134
  ];
1601
1135
  return [...new Set(targetValues)].map((target) => parseRuntimeTarget(target));
1602
1136
  }
1137
+ function parseSkillRenderTarget(value) {
1138
+ return parseRuntimeTarget(value);
1139
+ }
1603
1140
  function parseCsv(value) {
1604
1141
  if (typeof value !== "string" || value.trim() === "") {
1605
1142
  return [];
@@ -1609,6 +1146,16 @@ function parseCsv(value) {
1609
1146
  .map((item) => item.trim())
1610
1147
  .filter(Boolean);
1611
1148
  }
1149
+ function csvOption(value) {
1150
+ if (typeof value !== "string") {
1151
+ return undefined;
1152
+ }
1153
+ return parseCsv(value);
1154
+ }
1155
+ function singleValueArrayOption(value) {
1156
+ const option = stringOption(value);
1157
+ return option ? [option] : undefined;
1158
+ }
1612
1159
  function stringOption(value) {
1613
1160
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
1614
1161
  }
@@ -1648,18 +1195,6 @@ function parseRiskAcceptance(options) {
1648
1195
  });
1649
1196
  return Object.keys(acceptance).length > 0 ? acceptance : undefined;
1650
1197
  }
1651
- function parseProviderRouting(options) {
1652
- return {
1653
- provider: requireArg(options, "provider"),
1654
- model: requireArg(options, "model"),
1655
- fallbacks: parseCsv(options.fallbacks),
1656
- maxTokens: numberOption(options["max-tokens"], 0),
1657
- maxCostUsd: numberOption(options["max-cost-usd"], 0),
1658
- timeoutMs: numberOption(options["timeout-ms"], 30000),
1659
- retries: numberOption(options.retries, 0),
1660
- requiredCapabilities: parseCsv(options.capabilities),
1661
- };
1662
- }
1663
1198
  function parseBudgetEscalationDecision(options) {
1664
1199
  if (options["approve-budget-fallback"]) {
1665
1200
  return removeUndefined({
@@ -1677,13 +1212,6 @@ function parseBudgetEscalationDecision(options) {
1677
1212
  }
1678
1213
  return undefined;
1679
1214
  }
1680
- function parseApprovalDecision(options) {
1681
- return {
1682
- id: requireArg(options, "id"),
1683
- approver: requireArg(options, "approver"),
1684
- rationale: requireArg(options, "rationale"),
1685
- };
1686
- }
1687
1215
  function numberOption(value, fallback) {
1688
1216
  if (typeof value !== "string" || value.trim() === "") {
1689
1217
  return fallback;
@@ -1732,6 +1260,24 @@ function renderDependencyReport(report) {
1732
1260
  : report.dependencies.map((dependency) => `- ${dependency.id}: ${dependency.status} (${dependency.isComplete ? "complete" : "incomplete"})`)),
1733
1261
  ].join("\n");
1734
1262
  }
1263
+ function renderPreRunValidation(report) {
1264
+ const status = report.allowed ? "allowed" : "blocked";
1265
+ const missing = report.missing.length > 0 ? report.missing.join(", ") : "none";
1266
+ return [
1267
+ `Pre-run validation for ${report.taskId}: ${status}`,
1268
+ `- Ready: ${String(report.isReady)}`,
1269
+ `- Bypassed: ${String(report.isBypassed)}`,
1270
+ `- Missing: ${missing}`,
1271
+ `- Task: ${String(report.checks.task)}`,
1272
+ `- Estimate: ${String(report.checks.estimate)}`,
1273
+ `- Workflow run: ${String(report.checks.workflowRun)}`,
1274
+ `- Evidence: ${String(report.checks.evidence)}`,
1275
+ `- Review: ${String(report.checks.review)}`,
1276
+ ...(report.bypassDecisionArtifact
1277
+ ? [`- Bypass decision: ${report.bypassDecisionArtifact}`]
1278
+ : []),
1279
+ ].join("\n");
1280
+ }
1735
1281
  function renderTaskGraphPlan(plan) {
1736
1282
  return [
1737
1283
  "Task graph plan",
@@ -1749,6 +1295,28 @@ function renderTaskGraphPlan(plan) {
1749
1295
  ...taskGraphReadyLines(plan.complete),
1750
1296
  ].join("\n");
1751
1297
  }
1298
+ function renderTaskGraphDryRunPreview(preview) {
1299
+ return [
1300
+ "Task graph dry run",
1301
+ `- Mode: ${preview.mode}`,
1302
+ `- Would mutate: ${String(preview.wouldMutate)}`,
1303
+ `- Selected: ${preview.selectedTaskIds.join(", ")}`,
1304
+ "",
1305
+ "Tasks:",
1306
+ ...preview.tasks.map((task) => {
1307
+ const budget = task.budget
1308
+ ? ` budget=${task.budget.passed ? "pass" : "fail"} scopes=${task.budget.appliedBudgets.join(",") || "none"}`
1309
+ : " budget=not-configured";
1310
+ return `- ${task.id} ${task.title} (${task.ownerRole}) ${task.provider}/${task.model} fallbacks=${task.fallbacks.join(",") || "none"}${budget}`;
1311
+ }),
1312
+ "",
1313
+ "Locked:",
1314
+ ...taskGraphLockedLines(preview.locked),
1315
+ "",
1316
+ "Blocked:",
1317
+ ...taskGraphBlockedLines(preview.blocked),
1318
+ ].join("\n");
1319
+ }
1752
1320
  function renderTaskGraphBatchRun(batch) {
1753
1321
  return [
1754
1322
  "Task graph batch run",
@@ -1820,43 +1388,6 @@ function renderSummaryMarkdown(summary) {
1820
1388
  "",
1821
1389
  ].join("\n");
1822
1390
  }
1823
- function renderUsageReport(report) {
1824
- return [
1825
- `Usage${report.taskId ? ` for ${report.taskId}` : ""}`,
1826
- `- Requests: ${report.totals.requests}`,
1827
- `- Input tokens: ${report.totals.inputTokens}`,
1828
- `- Output tokens: ${report.totals.outputTokens}`,
1829
- `- Total tokens: ${report.totals.totalTokens}`,
1830
- `- Estimated cost USD: ${report.totals.estimatedCostUsd}`,
1831
- "",
1832
- "By role:",
1833
- ...usageLines(report.byRole),
1834
- "",
1835
- "By provider:",
1836
- ...usageLines(report.byProvider),
1837
- ].join("\n");
1838
- }
1839
- function renderBudgetCheck(result) {
1840
- return [
1841
- `Budget check${result.taskId ? ` for ${result.taskId}` : ""}: ${result.passed ? "passed" : "failed"}`,
1842
- `- Applied budgets: ${result.appliedBudgets.length > 0
1843
- ? result.appliedBudgets.join(", ")
1844
- : "none"}`,
1845
- `- Requests: ${result.usage.totals.requests}`,
1846
- `- Total tokens: ${result.usage.totals.totalTokens}`,
1847
- `- Estimated cost USD: ${result.usage.totals.estimatedCostUsd}`,
1848
- "",
1849
- "Violations:",
1850
- ...(result.violations.length === 0
1851
- ? ["- none"]
1852
- : result.violations.map((violation) => `- ${violation.scope} ${violation.metric}: ${violation.actual} > ${violation.limit}`)),
1853
- ].join("\n");
1854
- }
1855
- function usageLines(items) {
1856
- return items.length === 0
1857
- ? ["- none"]
1858
- : items.map((item) => `- ${item.key}: ${item.requests} requests, ${item.totalTokens} tokens, $${item.estimatedCostUsd}`);
1859
- }
1860
1391
  function renderPullRequestSummaryMarkdown(summary) {
1861
1392
  return [
1862
1393
  `# PR Summary: ${summary.task.id}`,
@@ -1973,6 +1504,8 @@ function renderTaskContextMarkdown(context) {
1973
1504
  : context.skills.selected.map((item) => `- ${item.skill.id} (score ${item.score}): ${item.rationale.join("; ")}`)),
1974
1505
  `- Source groups: ${context.skills.sourceGroups.join(", ") || "none"}`,
1975
1506
  "",
1507
+ renderMemoryPacket(context.memory),
1508
+ "",
1976
1509
  "## Decisions",
1977
1510
  ...eventLines(context.decisions),
1978
1511
  "",
@@ -2045,6 +1578,27 @@ function renderPlaywrightPlanMarkdown(plan) {
2045
1578
  "",
2046
1579
  ].join("\n");
2047
1580
  }
1581
+ function renderTaskDetails(task) {
1582
+ return [
1583
+ `ID: ${task.id}`,
1584
+ `Title: ${task.title}`,
1585
+ `Owner: ${task.ownerRole}`,
1586
+ `Status: ${task.status}`,
1587
+ `Goal: ${task.goal ?? "—"}`,
1588
+ `Scope: ${task.scope ?? "—"}`,
1589
+ `Acceptance Criteria: ${renderInlineList(task.acceptanceCriteria)}`,
1590
+ `Assumptions: ${renderInlineList(task.assumptions)}`,
1591
+ `Risks: ${renderInlineList(task.risks)}`,
1592
+ `Paths: ${renderInlineList(task.paths)}`,
1593
+ `Test Strategy: ${task.testStrategy ?? "—"}`,
1594
+ `Blocked Reason: ${task.blockedReason ?? "—"}`,
1595
+ `Created: ${task.createdAt}`,
1596
+ `Updated: ${task.updatedAt}`,
1597
+ ];
1598
+ }
1599
+ function renderInlineList(values) {
1600
+ return values && values.length > 0 ? values.join(", ") : "—";
1601
+ }
2048
1602
  function eventLines(events) {
2049
1603
  return events.length === 0
2050
1604
  ? ["- none"]