@jterrats/open-orchestra 0.4.1 → 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 (76) hide show
  1. package/dist/assets/web-console.js +91 -1
  2. package/dist/autonomous-phase-lifecycle.d.ts +3 -2
  3. package/dist/autonomous-phase-lifecycle.js +28 -8
  4. package/dist/autonomous-phase-lifecycle.js.map +1 -1
  5. package/dist/autonomous-run-state.d.ts +6 -4
  6. package/dist/autonomous-run-state.js +82 -11
  7. package/dist/autonomous-run-state.js.map +1 -1
  8. package/dist/autonomous-run-store.d.ts +5 -0
  9. package/dist/autonomous-run-store.js +27 -2
  10. package/dist/autonomous-run-store.js.map +1 -1
  11. package/dist/autonomous-workflow-constants.d.ts +1 -0
  12. package/dist/autonomous-workflow-constants.js.map +1 -1
  13. package/dist/autonomous-workflow.d.ts +2 -2
  14. package/dist/autonomous-workflow.js +2 -2
  15. package/dist/autonomous-workflow.js.map +1 -1
  16. package/dist/benchmark.js +16 -0
  17. package/dist/benchmark.js.map +1 -1
  18. package/dist/clarification.d.ts +2 -0
  19. package/dist/clarification.js +23 -5
  20. package/dist/clarification.js.map +1 -1
  21. package/dist/cli.js +89 -14
  22. package/dist/cli.js.map +1 -1
  23. package/dist/command-manifest.js +8 -0
  24. package/dist/command-manifest.js.map +1 -1
  25. package/dist/commands.d.ts +9 -2
  26. package/dist/commands.js +456 -47
  27. package/dist/commands.js.map +1 -1
  28. package/dist/constants.js +11 -0
  29. package/dist/constants.js.map +1 -1
  30. package/dist/mcp-oauth-proxy.d.ts +47 -0
  31. package/dist/mcp-oauth-proxy.js +276 -0
  32. package/dist/mcp-oauth-proxy.js.map +1 -1
  33. package/dist/memory.d.ts +11 -0
  34. package/dist/memory.js +224 -0
  35. package/dist/memory.js.map +1 -0
  36. package/dist/metrics-commands.js +9 -0
  37. package/dist/metrics-commands.js.map +1 -1
  38. package/dist/model-commands.js +18 -1
  39. package/dist/model-commands.js.map +1 -1
  40. package/dist/notifications.d.ts +25 -0
  41. package/dist/notifications.js +187 -11
  42. package/dist/notifications.js.map +1 -1
  43. package/dist/package-update-check.d.ts +19 -0
  44. package/dist/package-update-check.js +123 -0
  45. package/dist/package-update-check.js.map +1 -0
  46. package/dist/runtime-bootstrap.js +4 -2
  47. package/dist/runtime-bootstrap.js.map +1 -1
  48. package/dist/runtime-execution-renderer.js +3 -0
  49. package/dist/runtime-execution-renderer.js.map +1 -1
  50. package/dist/runtime-execution.js +6 -0
  51. package/dist/runtime-execution.js.map +1 -1
  52. package/dist/skills-commands.d.ts +1 -0
  53. package/dist/skills-commands.js +9 -1
  54. package/dist/skills-commands.js.map +1 -1
  55. package/dist/skills.d.ts +7 -1
  56. package/dist/skills.js +120 -11
  57. package/dist/skills.js.map +1 -1
  58. package/dist/subagent-protocol.js +6 -0
  59. package/dist/subagent-protocol.js.map +1 -1
  60. package/dist/tool-commands.d.ts +2 -0
  61. package/dist/tool-commands.js +105 -13
  62. package/dist/tool-commands.js.map +1 -1
  63. package/dist/types.d.ts +127 -4
  64. package/dist/types.js.map +1 -1
  65. package/dist/web-api.js +93 -1
  66. package/dist/web-api.js.map +1 -1
  67. package/dist/web-chart-contracts.d.ts +3 -1
  68. package/dist/web-chart-contracts.js +6 -0
  69. package/dist/web-chart-contracts.js.map +1 -1
  70. package/dist/web-console.js +2 -2
  71. package/dist/workflow-services.d.ts +21 -3
  72. package/dist/workflow-services.js +474 -10
  73. package/dist/workflow-services.js.map +1 -1
  74. package/package.json +2 -1
  75. package/skills/proactive-orchestra/SKILL.md +27 -0
  76. package/skills/proactive-orchestra/manifest.json +41 -0
package/dist/commands.js CHANGED
@@ -1,9 +1,11 @@
1
- import { initWorkspace } from "./workspace.js";
1
+ import { appendEvent, initWorkspace } from "./workspace.js";
2
2
  import { requireArg } from "./args.js";
3
- import { AUTONOMOUS_PHASE_SEQUENCE, autonomousRunsPath, checkArchitectSizing, closePhase, createAutonomousRun, initPhase, listAutonomousRuns, markRunDone, markRunFailed, readAutonomousRun, resumePhaseIndex, suspendPhaseForClarification, resumePhaseFromClarification, } from "./autonomous-workflow.js";
4
- import { listClarifications, openClarification, answerClarification, } from "./clarification.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";
5
7
  import { executePhaseWithLlm } from "./phase-executor.js";
6
- import { addEvidence, addPlaywrightEvidence, addTask, checkReadiness, checkTaskDependencies, claimLock, createHandoff, evaluateWorkflowGate, executeNextReadyTask, executePlanWithBudgetPreflight, executeReadyTaskBatch, generateExecutionPlan, generatePlaywrightTestPlan, generatePullRequestSummary, generateTaskGraphPlan, getWorkflowStatus, getWorkflowSummary, getTaskContext, getWorkflowConfig, listEvidence, listDecisions, listLocks, listReviews, listRoles, listTasks, recordReview, recordDecision, releaseLock, updateTask, } 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";
7
9
  import { validateWorkspace } from "./workspace-validator.js";
8
10
  import { getWebServerAddress, startWebApiServer } from "./web-api.js";
9
11
  import { decideTaskDelegation } from "./delegation-decision.js";
@@ -15,8 +17,8 @@ export { benchmarkCommand, estimateCommand } from "./metrics-commands.js";
15
17
  export { burndownCommand, calibrationCommand, sprintCommand, velocityCommand, } from "./sprint-commands.js";
16
18
  export { approvalsApproveCommand, approvalsListCommand, approvalsRejectCommand, approvalsShowCommand, budgetCheckCommand, configShowCommand, modelCompleteFakeCommand, modelProvidersCommand, modelProvenanceAddCommand, modelProvenanceListCommand, modelSetRoleCommand, usageCommand, } from "./model-commands.js";
17
19
  export { commandsManifestCommand, protocolBlockCommand, protocolRenderCommand, runtimeAdaptersCommand, runtimeBootstrapCommand, runtimeBriefCommand, runtimeDelegatePlanCommand, runtimeHandoffCommand, } from "./runtime-commands.js";
18
- export { lessonsAddCommand, lessonsListCommand, lessonsPromoteCommand, skillsListCommand, skillsPlanCommand, skillsRenderCommand, skillsValidateCommand, sourcesListCommand, } from "./skills-commands.js";
19
- export { diagramsLintCommand, mcpOAuthProxyEvaluateCommand, } from "./tool-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";
20
22
  export { telemetryDisableCommand, telemetryEnableCommand, telemetryEvalDatasetCommand, telemetryExportCommand, telemetryStatusCommand, telemetrySubmitCommand, } from "./telemetry-commands.js";
21
23
  import { buildPrBody, createPullRequest } from "./github.js";
22
24
  export async function initCommand(options, io) {
@@ -63,6 +65,23 @@ export async function statusCommand(options, io) {
63
65
  }
64
66
  }
65
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
+ }
66
85
  const report = await validateWorkspace();
67
86
  if (options.json) {
68
87
  io.log(JSON.stringify(report, null, 2));
@@ -114,7 +133,11 @@ export async function taskAddCommand(options, io) {
114
133
  io.log(`Added task ${task.id}`);
115
134
  }
116
135
  export async function taskListCommand(options, io) {
117
- 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);
118
141
  if (options.json) {
119
142
  io.log(JSON.stringify(tasks, null, 2));
120
143
  return;
@@ -127,6 +150,67 @@ export async function taskListCommand(options, io) {
127
150
  io.log(`${task.id} [${task.status}] ${task.title} (${task.ownerRole})`);
128
151
  }
129
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
+ }
130
214
  export async function rolesListCommand(options, io) {
131
215
  const roles = await listRoles();
132
216
  if (options.json) {
@@ -146,6 +230,15 @@ export async function taskUpdateCommand(options, io) {
146
230
  const id = requireArg(options, "id");
147
231
  await updateTask(removeUndefined({
148
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"]),
149
242
  status: stringOption(options.status),
150
243
  blockedReason: stringOption(options["blocked-reason"]),
151
244
  }));
@@ -168,6 +261,15 @@ export async function graphPlanCommand(options, io) {
168
261
  io.log(renderTaskGraphPlan(plan));
169
262
  }
170
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
+ }
171
273
  const run = await executeNextReadyTask();
172
274
  if (options.json) {
173
275
  io.log(JSON.stringify(run, null, 2));
@@ -176,6 +278,15 @@ export async function graphRunNextCommand(options, io) {
176
278
  io.log(renderExecutionRunMarkdown(run));
177
279
  }
178
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
+ }
179
290
  const batch = await executeReadyTaskBatch();
180
291
  if (options.json) {
181
292
  io.log(JSON.stringify(batch, null, 2));
@@ -248,6 +359,7 @@ export async function handoffCommand(options, io) {
248
359
  gaps: stringOption(options.gaps),
249
360
  risks: stringOption(options.risks),
250
361
  playwright: stringOption(options.playwright),
362
+ updateOwner: options["update-owner"] ? true : undefined,
251
363
  }));
252
364
  io.log(`Created ${artifact}`);
253
365
  }
@@ -367,6 +479,40 @@ export async function contextCommand(options, io) {
367
479
  }
368
480
  io.log(renderTaskContextMarkdown(context));
369
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
+ }
370
516
  export async function planCommand(options, io) {
371
517
  const plan = await generateExecutionPlan(requireArg(options, "task"));
372
518
  if (options.json) {
@@ -377,7 +523,12 @@ export async function planCommand(options, io) {
377
523
  }
378
524
  export async function runCommand(options, io) {
379
525
  const taskId = requireArg(options, "task");
380
- 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
+ }));
381
532
  if (options.json) {
382
533
  io.log(JSON.stringify(run, null, 2));
383
534
  return;
@@ -489,12 +640,22 @@ export async function workflowRunCommand(options, io) {
489
640
  const taskId = requireArg(options, "task");
490
641
  const gates = (stringOption(options.gates) ?? "phase");
491
642
  const maxIterations = numberOption(options["max-iterations"], 5);
643
+ const timeoutMinutes = numberOption(options["timeout-minutes"], 0);
492
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;
493
648
  if (!GATE_MODES.includes(gates)) {
494
649
  throw new Error(`--gates must be one of: ${GATE_MODES.join(", ")}`);
495
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
+ }
496
657
  if (options["dry-run"]) {
497
- return workflowDryRun(options, io, taskId, gates, maxIterations);
658
+ return workflowDryRun(options, io, taskId, gates, maxIterations, phaseSelection);
498
659
  }
499
660
  let run;
500
661
  let startIndex;
@@ -503,21 +664,39 @@ export async function workflowRunCommand(options, io) {
503
664
  if (!existing)
504
665
  throw new Error(`workflow run not found: ${String(options.resume)}`);
505
666
  run = existing;
506
- 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);
507
672
  if (startIndex === -1) {
508
673
  io.log(`Workflow ${run.id} is already complete`);
509
674
  if (options.json)
510
675
  io.log(JSON.stringify({ run, file, cwd }, null, 2));
511
676
  return;
512
677
  }
513
- 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) };
514
684
  }
515
685
  else {
516
- 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
+ });
517
693
  startIndex = 0;
518
694
  io.log(`Started autonomous workflow ${run.id} for task ${taskId} [gates=${gates}]`);
519
695
  }
520
- 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
+ });
521
700
  const result = { run, file, cwd };
522
701
  if (options.json) {
523
702
  io.log(JSON.stringify(result, null, 2));
@@ -535,7 +714,9 @@ export async function workflowRunCommand(options, io) {
535
714
  }
536
715
  export async function workflowRunListCommand(options, io) {
537
716
  const cwd = process.cwd();
538
- const runs = await listAutonomousRuns(cwd);
717
+ const runs = options.active
718
+ ? await listActiveAutonomousRuns(cwd)
719
+ : await listAutonomousRuns(cwd);
539
720
  if (options.json) {
540
721
  io.log(JSON.stringify(runs, null, 2));
541
722
  return;
@@ -551,17 +732,35 @@ export async function workflowRunListCommand(options, io) {
551
732
  io.log(` ${phases}`);
552
733
  }
553
734
  }
554
- const CLARIFICATION_TARGETS = ["po", "architect"];
555
- 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
+ }
556
758
  export async function workflowClarifyCommand(options, io) {
557
759
  const cwd = process.cwd();
558
760
  const runId = requireArg(options, "run");
559
- const from = requireArg(options, "from");
560
- const to = requireArg(options, "to");
761
+ const from = await resolveClarificationRole(cwd, requireArg(options, "from"));
762
+ const to = await resolveClarificationRole(cwd, requireArg(options, "to"));
561
763
  const question = requireArg(options, "question");
562
- if (!CLARIFICATION_TARGETS.includes(to)) {
563
- throw new Error(`--to must be one of: ${CLARIFICATION_TARGETS.join(", ")}`);
564
- }
565
764
  const run = await readAutonomousRun(cwd, runId);
566
765
  if (!run)
567
766
  throw new Error(`workflow run not found: ${runId}`);
@@ -572,9 +771,6 @@ export async function workflowClarifyCommand(options, io) {
572
771
  throw new Error(`no active phase found for role ${from} in run ${runId}`);
573
772
  }
574
773
  const activePhase = run.phases[activePhaseIdx];
575
- if (!CLARIFICATION_ALLOWED_PHASES.has(activePhase.phase)) {
576
- throw new Error(`clarification is only allowed from developer or qa phases (active: ${activePhase.phase})`);
577
- }
578
774
  // Open clarification record
579
775
  const record = await openClarification(cwd, {
580
776
  runId,
@@ -585,8 +781,9 @@ export async function workflowClarifyCommand(options, io) {
585
781
  });
586
782
  // Suspend the phase if it's still running
587
783
  if (activePhase.status === "running") {
588
- const phaseSeqIdx = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.phase === activePhase.phase);
589
- 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);
590
787
  }
591
788
  if (options.json) {
592
789
  io.log(JSON.stringify(record, null, 2));
@@ -610,11 +807,12 @@ export async function workflowClarifyRespondCommand(options, io) {
610
807
  answer,
611
808
  });
612
809
  // Resume the suspended phase back to running
613
- 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);
614
812
  if (phaseSeqIdx !== -1) {
615
813
  const phase = run.phases.find((p) => p.role === record.fromRole);
616
814
  if (phase?.status === "awaiting_clarification") {
617
- await resumePhaseFromClarification(cwd, run, phaseSeqIdx);
815
+ await resumePhaseFromClarification(cwd, run, phaseSeqIdx, sequence);
618
816
  }
619
817
  }
620
818
  if (options.json) {
@@ -628,14 +826,25 @@ export async function workflowClarifyRespondCommand(options, io) {
628
826
  }
629
827
  export async function workflowClarifyListCommand(options, io) {
630
828
  const cwd = process.cwd();
631
- const runId = requireArg(options, "run");
632
- 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);
633
840
  if (options.json) {
634
841
  io.log(JSON.stringify(clarifications, null, 2));
635
842
  return;
636
843
  }
637
844
  if (clarifications.length === 0) {
638
- io.log(`No clarifications for run ${runId}`);
845
+ io.log(runId
846
+ ? `No clarifications for run ${runId}`
847
+ : `No clarifications for task ${taskId}`);
639
848
  return;
640
849
  }
641
850
  for (const c of clarifications) {
@@ -646,17 +855,115 @@ export async function workflowClarifyListCommand(options, io) {
646
855
  io.log(` A: ${c.answer}`);
647
856
  }
648
857
  }
649
- const DEV_PHASE_INDEX = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.phase === "developer");
650
- 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) {
651
950
  let current = run;
652
951
  let qaFailNotes;
653
- for (let i = startIndex; i < AUTONOMOUS_PHASE_SEQUENCE.length; i++) {
654
- 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];
655
962
  // Init phase task (skip if already has a running/done/clarification record from resume)
656
963
  const existing = current.phases.find((p) => p.phase === def.phase && p.status !== "qa_failed");
657
964
  if (!existing || existing.status === "pending") {
658
965
  const retryContext = def.phase === "developer" && qaFailNotes ? qaFailNotes : undefined;
659
- const phaseRecord = await initPhase(cwd, current, i, retryContext);
966
+ const phaseRecord = await initPhase(cwd, current, i, retryContext, sequence);
660
967
  current = { ...current, phases: [...current.phases, phaseRecord] };
661
968
  if (retryContext) {
662
969
  io.log(`↺ ${def.phase} (${def.role}) retry — QA findings: ${retryContext.slice(0, 80)}`);
@@ -682,7 +989,7 @@ async function executePhases(cwd, run, startIndex, io) {
682
989
  }).catch(async (error) => {
683
990
  const message = error instanceof Error ? error.message : String(error);
684
991
  io.log(`✗ ${def.phase} provider execution failed: ${message}`);
685
- current = await markRunFailed(cwd, current, message);
992
+ current = await markRunFailed(cwd, current, message, def.phase);
686
993
  return null;
687
994
  });
688
995
  if (!llmResult)
@@ -690,6 +997,17 @@ async function executePhases(cwd, run, startIndex, io) {
690
997
  if (llmResult.mode === "llm") {
691
998
  io.log(` ✓ llm artifact (${llmResult.artifact})`);
692
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
+ }
693
1011
  // Architect sizing gate — always enforced regardless of --gates mode.
694
1012
  // In LLM mode, the architect phase records the sizing decision before this check.
695
1013
  if (def.phase === "architect") {
@@ -705,15 +1023,15 @@ async function executePhases(cwd, run, startIndex, io) {
705
1023
  io.log(` ✓ sizing=${sizing.sizing}${sizing.points !== undefined ? ` (${sizing.points} pts)` : ""}`);
706
1024
  }
707
1025
  const outcome = llmResult.outcome;
708
- const result = await closePhase(cwd, current, i, outcome);
1026
+ const result = await closePhase(cwd, current, i, outcome, sequence);
709
1027
  current = result.run;
710
1028
  if (result.handoffArtifact) {
711
- io.log(` ✓ handoff → ${AUTONOMOUS_PHASE_SEQUENCE[i + 1]?.role ?? "end"} (${result.handoffArtifact})`);
1029
+ io.log(` ✓ handoff → ${sequence[i + 1]?.role ?? "end"} (${result.handoffArtifact})`);
712
1030
  }
713
1031
  if (result.reviewArtifact) {
714
- const nextPhase = AUTONOMOUS_PHASE_SEQUENCE[i + 1]?.phase;
1032
+ const nextPhase = sequence[i + 1]?.phase;
715
1033
  io.log(` ⏸ gate ${def.phase}→${nextPhase} — review: ${result.reviewArtifact}`);
716
- 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>"`);
717
1035
  io.log(``);
718
1036
  io.log(`╔══ GATE PAUSE: ${def.phase.toUpperCase()} → ${nextPhase?.toUpperCase()} ══════════════════════`);
719
1037
  io.log(`║ Run: ${current.id}`);
@@ -721,6 +1039,8 @@ async function executePhases(cwd, run, startIndex, io) {
721
1039
  io.log(`║ Review: ${result.reviewArtifact}`);
722
1040
  io.log(`║`);
723
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:`);
724
1044
  io.log(`║ orchestra workflow run --task ${current.taskId} --resume ${current.id}`);
725
1045
  io.log(`╚══════════════════════════════════════════════════════════════`);
726
1046
  return current;
@@ -730,7 +1050,7 @@ async function executePhases(cwd, run, startIndex, io) {
730
1050
  if (closedPhase) {
731
1051
  qaFailNotes = closedPhase.notes ?? "QA findings — see QA phase artifact";
732
1052
  io.log(` ✗ qa failed (iteration ${current.qaIterations}/${current.maxIterations}) — routing back to developer`);
733
- 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
734
1054
  continue;
735
1055
  }
736
1056
  // Release phase: auto-create PR if configured
@@ -761,18 +1081,29 @@ async function executePhases(cwd, run, startIndex, io) {
761
1081
  }
762
1082
  }
763
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
+ }
764
1095
  }
765
1096
  current = await markRunDone(cwd, current);
766
1097
  return current;
767
1098
  }
768
- async function workflowDryRun(options, io, taskId, gates, maxIterations) {
1099
+ async function workflowDryRun(options, io, taskId, gates, maxIterations, phaseSelection) {
769
1100
  const gateTransitions = new Set(["po→architect", "qa→release"]);
770
1101
  io.log(`Dry run — no records will be created`);
771
1102
  io.log(`Task: ${taskId} gates: ${gates} max-iterations: ${maxIterations}`);
772
1103
  io.log(``);
773
- for (let i = 0; i < AUTONOMOUS_PHASE_SEQUENCE.length; i++) {
774
- const def = AUTONOMOUS_PHASE_SEQUENCE[i];
775
- 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];
776
1107
  const transitionKey = next ? `${def.phase}→${next.phase}` : "";
777
1108
  const gateLabel = gates === "all"
778
1109
  ? "gate=yes"
@@ -781,13 +1112,18 @@ async function workflowDryRun(options, io, taskId, gates, maxIterations) {
781
1112
  : "gate=no";
782
1113
  io.log(` ${def.phase} (${def.role}) ${gateLabel}`);
783
1114
  }
1115
+ if (phaseSelection.skipped.length > 0) {
1116
+ io.log(``);
1117
+ io.log(`Skipped: ${phaseSelection.skipped.map((phase) => phase.phase).join(", ")}`);
1118
+ }
784
1119
  if (options.json) {
785
1120
  io.log(JSON.stringify({
786
1121
  dryRun: true,
787
1122
  taskId,
788
1123
  gates,
789
1124
  maxIterations,
790
- phases: AUTONOMOUS_PHASE_SEQUENCE,
1125
+ phases: phaseSelection.sequence,
1126
+ skipped: phaseSelection.skipped,
791
1127
  }, null, 2));
792
1128
  }
793
1129
  }
@@ -810,6 +1146,16 @@ function parseCsv(value) {
810
1146
  .map((item) => item.trim())
811
1147
  .filter(Boolean);
812
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
+ }
813
1159
  function stringOption(value) {
814
1160
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
815
1161
  }
@@ -914,6 +1260,24 @@ function renderDependencyReport(report) {
914
1260
  : report.dependencies.map((dependency) => `- ${dependency.id}: ${dependency.status} (${dependency.isComplete ? "complete" : "incomplete"})`)),
915
1261
  ].join("\n");
916
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
+ }
917
1281
  function renderTaskGraphPlan(plan) {
918
1282
  return [
919
1283
  "Task graph plan",
@@ -931,6 +1295,28 @@ function renderTaskGraphPlan(plan) {
931
1295
  ...taskGraphReadyLines(plan.complete),
932
1296
  ].join("\n");
933
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
+ }
934
1320
  function renderTaskGraphBatchRun(batch) {
935
1321
  return [
936
1322
  "Task graph batch run",
@@ -1118,6 +1504,8 @@ function renderTaskContextMarkdown(context) {
1118
1504
  : context.skills.selected.map((item) => `- ${item.skill.id} (score ${item.score}): ${item.rationale.join("; ")}`)),
1119
1505
  `- Source groups: ${context.skills.sourceGroups.join(", ") || "none"}`,
1120
1506
  "",
1507
+ renderMemoryPacket(context.memory),
1508
+ "",
1121
1509
  "## Decisions",
1122
1510
  ...eventLines(context.decisions),
1123
1511
  "",
@@ -1190,6 +1578,27 @@ function renderPlaywrightPlanMarkdown(plan) {
1190
1578
  "",
1191
1579
  ].join("\n");
1192
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
+ }
1193
1602
  function eventLines(events) {
1194
1603
  return events.length === 0
1195
1604
  ? ["- none"]