@jterrats/open-orchestra 0.4.1 → 0.4.2-beta.2

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 (100) hide show
  1. package/AGENTS.md +5 -3
  2. package/dist/advisory-artifacts.d.ts +14 -0
  3. package/dist/advisory-artifacts.js +100 -0
  4. package/dist/advisory-artifacts.js.map +1 -0
  5. package/dist/assets/web-console.js +230 -1
  6. package/dist/autonomous-phase-lifecycle.d.ts +3 -2
  7. package/dist/autonomous-phase-lifecycle.js +28 -8
  8. package/dist/autonomous-phase-lifecycle.js.map +1 -1
  9. package/dist/autonomous-run-state.d.ts +6 -4
  10. package/dist/autonomous-run-state.js +82 -11
  11. package/dist/autonomous-run-state.js.map +1 -1
  12. package/dist/autonomous-run-store.d.ts +5 -0
  13. package/dist/autonomous-run-store.js +27 -2
  14. package/dist/autonomous-run-store.js.map +1 -1
  15. package/dist/autonomous-workflow-constants.d.ts +1 -0
  16. package/dist/autonomous-workflow-constants.js.map +1 -1
  17. package/dist/autonomous-workflow.d.ts +2 -2
  18. package/dist/autonomous-workflow.js +2 -2
  19. package/dist/autonomous-workflow.js.map +1 -1
  20. package/dist/benchmark.js +16 -0
  21. package/dist/benchmark.js.map +1 -1
  22. package/dist/clarification.d.ts +2 -0
  23. package/dist/clarification.js +23 -5
  24. package/dist/clarification.js.map +1 -1
  25. package/dist/cli.js +97 -18
  26. package/dist/cli.js.map +1 -1
  27. package/dist/command-manifest.d.ts +3 -0
  28. package/dist/command-manifest.js +17 -1
  29. package/dist/command-manifest.js.map +1 -1
  30. package/dist/commands.d.ts +10 -2
  31. package/dist/commands.js +484 -49
  32. package/dist/commands.js.map +1 -1
  33. package/dist/constants.js +13 -0
  34. package/dist/constants.js.map +1 -1
  35. package/dist/context-budget.d.ts +4 -0
  36. package/dist/context-budget.js +119 -0
  37. package/dist/context-budget.js.map +1 -0
  38. package/dist/mcp-oauth-proxy.d.ts +79 -0
  39. package/dist/mcp-oauth-proxy.js +396 -0
  40. package/dist/mcp-oauth-proxy.js.map +1 -1
  41. package/dist/memory.d.ts +11 -0
  42. package/dist/memory.js +224 -0
  43. package/dist/memory.js.map +1 -0
  44. package/dist/metrics-commands.js +9 -0
  45. package/dist/metrics-commands.js.map +1 -1
  46. package/dist/model-commands.js +18 -1
  47. package/dist/model-commands.js.map +1 -1
  48. package/dist/notifications.d.ts +25 -0
  49. package/dist/notifications.js +187 -11
  50. package/dist/notifications.js.map +1 -1
  51. package/dist/package-update-check.d.ts +19 -0
  52. package/dist/package-update-check.js +123 -0
  53. package/dist/package-update-check.js.map +1 -0
  54. package/dist/runtime-bootstrap.js +4 -2
  55. package/dist/runtime-bootstrap.js.map +1 -1
  56. package/dist/runtime-commands.js +10 -0
  57. package/dist/runtime-commands.js.map +1 -1
  58. package/dist/runtime-execution-renderer.js +5 -0
  59. package/dist/runtime-execution-renderer.js.map +1 -1
  60. package/dist/runtime-execution.d.ts +4 -2
  61. package/dist/runtime-execution.js +17 -4
  62. package/dist/runtime-execution.js.map +1 -1
  63. package/dist/setup-agents-import.d.ts +42 -0
  64. package/dist/setup-agents-import.js +337 -0
  65. package/dist/setup-agents-import.js.map +1 -0
  66. package/dist/skills-commands.d.ts +1 -0
  67. package/dist/skills-commands.js +9 -1
  68. package/dist/skills-commands.js.map +1 -1
  69. package/dist/skills.d.ts +7 -1
  70. package/dist/skills.js +120 -11
  71. package/dist/skills.js.map +1 -1
  72. package/dist/subagent-protocol.js +6 -0
  73. package/dist/subagent-protocol.js.map +1 -1
  74. package/dist/tool-commands.d.ts +2 -0
  75. package/dist/tool-commands.js +113 -13
  76. package/dist/tool-commands.js.map +1 -1
  77. package/dist/types.d.ts +163 -4
  78. package/dist/types.js.map +1 -1
  79. package/dist/web-api.js +216 -1
  80. package/dist/web-api.js.map +1 -1
  81. package/dist/web-chart-contracts.d.ts +3 -1
  82. package/dist/web-chart-contracts.js +6 -0
  83. package/dist/web-chart-contracts.js.map +1 -1
  84. package/dist/web-console.js +2 -2
  85. package/dist/workflow-services.d.ts +24 -4
  86. package/dist/workflow-services.js +478 -12
  87. package/dist/workflow-services.js.map +1 -1
  88. package/dist/workflow-templates.d.ts +1 -0
  89. package/dist/workflow-templates.js +1 -0
  90. package/dist/workflow-templates.js.map +1 -1
  91. package/dist/workspace.js +6 -0
  92. package/dist/workspace.js.map +1 -1
  93. package/docs/command-contracts.md +22 -0
  94. package/docs/mcp-oauth-proxy-evaluation.md +14 -0
  95. package/docs/runtime-adapters.md +4 -0
  96. package/docs/setup-agents-bridge.md +61 -0
  97. package/docs/traceability-flow.md +89 -0
  98. package/package.json +8 -6
  99. package/skills/proactive-orchestra/SKILL.md +27 -0
  100. package/skills/proactive-orchestra/manifest.json +41 -0
package/dist/commands.js CHANGED
@@ -1,22 +1,25 @@
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";
10
12
  import { listCollaborationFlows, recommendCollaborationFlow, } from "./collaboration-flows.js";
11
13
  import { listWorkflowTemplates, renderWorkflowTemplates, selectWorkflowTemplates, validateWorkflowTemplates, } from "./workflow-templates.js";
12
14
  import { parseRuntimeTarget } from "./runtime-adapters.js";
15
+ import { importSetupAgentsArtifacts } from "./setup-agents-import.js";
13
16
  export { instructionsApplyCommand, instructionsBlockCommand, instructionsImportsCommand, instructionsStaleCommand, } from "./instruction-commands.js";
14
17
  export { benchmarkCommand, estimateCommand } from "./metrics-commands.js";
15
18
  export { burndownCommand, calibrationCommand, sprintCommand, velocityCommand, } from "./sprint-commands.js";
16
19
  export { approvalsApproveCommand, approvalsListCommand, approvalsRejectCommand, approvalsShowCommand, budgetCheckCommand, configShowCommand, modelCompleteFakeCommand, modelProvidersCommand, modelProvenanceAddCommand, modelProvenanceListCommand, modelSetRoleCommand, usageCommand, } from "./model-commands.js";
17
20
  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";
21
+ export { lessonsAddCommand, lessonsDeleteCommand, lessonsListCommand, lessonsPromoteCommand, skillsListCommand, skillsPlanCommand, skillsRenderCommand, skillsValidateCommand, sourcesListCommand, } from "./skills-commands.js";
22
+ export { diagramsLintCommand, mcpOAuthProxyEvaluateCommand, mcpOAuthProxyStartCommand, mcpOAuthProxyTokenCommand, } from "./tool-commands.js";
20
23
  export { telemetryDisableCommand, telemetryEnableCommand, telemetryEvalDatasetCommand, telemetryExportCommand, telemetryStatusCommand, telemetrySubmitCommand, } from "./telemetry-commands.js";
21
24
  import { buildPrBody, createPullRequest } from "./github.js";
22
25
  export async function initCommand(options, io) {
@@ -63,6 +66,23 @@ export async function statusCommand(options, io) {
63
66
  }
64
67
  }
65
68
  export async function validateCommand(options, io) {
69
+ if (options["pre-run"]) {
70
+ const report = await validatePreRun(requireArg(options, "task"), removeUndefined({
71
+ bypass: Boolean(options.bypass),
72
+ bypassOwner: stringOption(options["bypass-owner"]),
73
+ bypassRationale: stringOption(options["bypass-rationale"]),
74
+ }));
75
+ if (options.json) {
76
+ io.log(JSON.stringify(report, null, 2));
77
+ }
78
+ else {
79
+ io.log(renderPreRunValidation(report));
80
+ }
81
+ if (!report.allowed) {
82
+ throw new Error(`pre-run validation failed: ${report.missing.join(", ")}`);
83
+ }
84
+ return;
85
+ }
66
86
  const report = await validateWorkspace();
67
87
  if (options.json) {
68
88
  io.log(JSON.stringify(report, null, 2));
@@ -113,8 +133,27 @@ export async function taskAddCommand(options, io) {
113
133
  const task = await addTask(input);
114
134
  io.log(`Added task ${task.id}`);
115
135
  }
136
+ export async function setupAgentsImportCommand(options, io) {
137
+ const report = await importSetupAgentsArtifacts(removeUndefined({
138
+ source: stringOption(options.source),
139
+ }));
140
+ if (options.json) {
141
+ io.log(JSON.stringify(report, null, 2));
142
+ return;
143
+ }
144
+ io.log(`Imported setup-agents artifacts from ${report.sourceRoot}`);
145
+ io.log(`Profiles: ${report.profiles.imported.length} imported, ${report.profiles.conflicts.length} conflicts`);
146
+ io.log(`Tasks: ${report.tasks.imported.length} imported, ${report.tasks.skipped.length} skipped, ${report.tasks.conflicts.length} conflicts`);
147
+ const evidenceCount = report.evidence.imported.reduce((total, item) => total + item.ids.length, 0);
148
+ const handoffCount = report.handoffs.imported.reduce((total, item) => total + item.ids.length, 0);
149
+ io.log(`References: ${evidenceCount} evidence, ${handoffCount} handoffs preserved`);
150
+ }
116
151
  export async function taskListCommand(options, io) {
117
- const tasks = await listTasks();
152
+ if (options.help || options.h) {
153
+ io.log("task list [--json] [--status <csv>] [--owner <role>] [--filter <text>] [--top-level] [--include-archived]");
154
+ return;
155
+ }
156
+ const tasks = filterTasks(await listTasks(), options);
118
157
  if (options.json) {
119
158
  io.log(JSON.stringify(tasks, null, 2));
120
159
  return;
@@ -127,6 +166,67 @@ export async function taskListCommand(options, io) {
127
166
  io.log(`${task.id} [${task.status}] ${task.title} (${task.ownerRole})`);
128
167
  }
129
168
  }
169
+ function filterTasks(tasks, options) {
170
+ const statuses = parseTaskStatusFilter(options.status);
171
+ const owner = stringOption(options.owner);
172
+ const textFilter = stringOption(options.filter)?.toLowerCase();
173
+ return tasks.filter((task) => matchesStatus(task, statuses) &&
174
+ (options["include-archived"] || task.status !== "archived") &&
175
+ (!owner || task.ownerRole === owner) &&
176
+ (!textFilter || taskSearchText(task).includes(textFilter)) &&
177
+ (!options["top-level"] || isTopLevelTask(task)));
178
+ }
179
+ function parseTaskStatusFilter(value) {
180
+ const statuses = parseCsv(value);
181
+ const invalid = statuses.filter((status) => !TASK_STATUSES.includes(status));
182
+ if (invalid.length > 0) {
183
+ throw new Error(`unknown task status: ${invalid.join(", ")}. Valid statuses: ${TASK_STATUSES.join(", ")}`);
184
+ }
185
+ return new Set(statuses);
186
+ }
187
+ function matchesStatus(task, statuses) {
188
+ return statuses.size === 0 || statuses.has(task.status);
189
+ }
190
+ function taskSearchText(task) {
191
+ return `${task.id} ${task.title}`.toLowerCase();
192
+ }
193
+ function isTopLevelTask(task) {
194
+ return !["pm", "po", "architect", "developer", "qa", "release"].some((phase) => task.id.includes(`-${phase}-`));
195
+ }
196
+ export async function taskShowCommand(options, io) {
197
+ const id = requireArg(options, "id");
198
+ const task = (await listTasks()).find((candidate) => candidate.id === id);
199
+ if (!task) {
200
+ throw new Error(`unknown task: ${id}`);
201
+ }
202
+ if (options.json) {
203
+ io.log(JSON.stringify(task, null, 2));
204
+ return;
205
+ }
206
+ for (const line of renderTaskDetails(task)) {
207
+ io.log(line);
208
+ }
209
+ }
210
+ export async function taskDeleteCommand(options, io) {
211
+ const task = await deleteTask(requireArg(options, "id"), {
212
+ force: Boolean(options.force),
213
+ });
214
+ if (options.json) {
215
+ io.log(JSON.stringify(task, null, 2));
216
+ return;
217
+ }
218
+ io.log(`Deleted task ${task.id}`);
219
+ }
220
+ export async function taskArchiveCommand(options, io) {
221
+ const task = await archiveTask(requireArg(options, "id"), {
222
+ force: Boolean(options.force),
223
+ });
224
+ if (options.json) {
225
+ io.log(JSON.stringify(task, null, 2));
226
+ return;
227
+ }
228
+ io.log(`Archived task ${task.id}`);
229
+ }
130
230
  export async function rolesListCommand(options, io) {
131
231
  const roles = await listRoles();
132
232
  if (options.json) {
@@ -146,6 +246,15 @@ export async function taskUpdateCommand(options, io) {
146
246
  const id = requireArg(options, "id");
147
247
  await updateTask(removeUndefined({
148
248
  id,
249
+ title: stringOption(options.title),
250
+ ownerRole: stringOption(options.owner),
251
+ goal: stringOption(options.goal),
252
+ scope: stringOption(options.scope),
253
+ paths: csvOption(options.paths),
254
+ acceptanceCriteria: singleValueArrayOption(options.acceptance),
255
+ assumptions: singleValueArrayOption(options.assumptions),
256
+ risks: singleValueArrayOption(options.risks),
257
+ testStrategy: stringOption(options["test-strategy"]),
149
258
  status: stringOption(options.status),
150
259
  blockedReason: stringOption(options["blocked-reason"]),
151
260
  }));
@@ -168,6 +277,15 @@ export async function graphPlanCommand(options, io) {
168
277
  io.log(renderTaskGraphPlan(plan));
169
278
  }
170
279
  export async function graphRunNextCommand(options, io) {
280
+ if (options["dry-run"]) {
281
+ const preview = await previewTaskGraphRun("run-next");
282
+ if (options.json) {
283
+ io.log(JSON.stringify(preview, null, 2));
284
+ return;
285
+ }
286
+ io.log(renderTaskGraphDryRunPreview(preview));
287
+ return;
288
+ }
171
289
  const run = await executeNextReadyTask();
172
290
  if (options.json) {
173
291
  io.log(JSON.stringify(run, null, 2));
@@ -176,6 +294,15 @@ export async function graphRunNextCommand(options, io) {
176
294
  io.log(renderExecutionRunMarkdown(run));
177
295
  }
178
296
  export async function graphRunReadyCommand(options, io) {
297
+ if (options["dry-run"]) {
298
+ const preview = await previewTaskGraphRun("run-ready");
299
+ if (options.json) {
300
+ io.log(JSON.stringify(preview, null, 2));
301
+ return;
302
+ }
303
+ io.log(renderTaskGraphDryRunPreview(preview));
304
+ return;
305
+ }
179
306
  const batch = await executeReadyTaskBatch();
180
307
  if (options.json) {
181
308
  io.log(JSON.stringify(batch, null, 2));
@@ -248,6 +375,7 @@ export async function handoffCommand(options, io) {
248
375
  gaps: stringOption(options.gaps),
249
376
  risks: stringOption(options.risks),
250
377
  playwright: stringOption(options.playwright),
378
+ updateOwner: options["update-owner"] ? true : undefined,
251
379
  }));
252
380
  io.log(`Created ${artifact}`);
253
381
  }
@@ -360,13 +488,49 @@ export async function delegationDecideCommand(options, io) {
360
488
  io.log(renderDelegationDecisionMarkdown(decision));
361
489
  }
362
490
  export async function contextCommand(options, io) {
363
- const context = await getTaskContext(requireArg(options, "task"));
491
+ const context = await getTaskContext(requireArg(options, "task"), process.cwd(), {
492
+ tokenBudget: numberOption(options.budget, 3000),
493
+ });
364
494
  if (options.json) {
365
495
  io.log(JSON.stringify(context, null, 2));
366
496
  return;
367
497
  }
368
498
  io.log(renderTaskContextMarkdown(context));
369
499
  }
500
+ export async function memoryQueryCommand(options, io) {
501
+ const hook = parseMemoryHook(stringOption(options.hook));
502
+ const taskId = stringOption(options.task);
503
+ const query = stringOption(options.query);
504
+ const packet = await queryMemory({
505
+ ...(taskId ? { taskId } : {}),
506
+ hook,
507
+ ...(query ? { query } : {}),
508
+ tokenBudget: numberOption(options.budget, 900),
509
+ });
510
+ await recordMemoryEvent(process.cwd(), packet, "MEMORY_QUERIED");
511
+ if (options.json) {
512
+ io.log(JSON.stringify(packet, null, 2));
513
+ return;
514
+ }
515
+ io.log(renderMemoryPacket(packet));
516
+ }
517
+ export async function memoryHookCommand(options, io) {
518
+ const point = parseMemoryHook(requireArg(options, "point"));
519
+ const taskId = stringOption(options.task);
520
+ const query = stringOption(options.query);
521
+ const packet = await queryMemory({
522
+ ...(taskId ? { taskId } : {}),
523
+ hook: point,
524
+ ...(query ? { query } : {}),
525
+ tokenBudget: numberOption(options.budget, 900),
526
+ });
527
+ await recordMemoryEvent(process.cwd(), packet, "MEMORY_HOOK_RAN");
528
+ if (options.json) {
529
+ io.log(JSON.stringify(packet, null, 2));
530
+ return;
531
+ }
532
+ io.log(renderMemoryPacket(packet));
533
+ }
370
534
  export async function planCommand(options, io) {
371
535
  const plan = await generateExecutionPlan(requireArg(options, "task"));
372
536
  if (options.json) {
@@ -377,7 +541,12 @@ export async function planCommand(options, io) {
377
541
  }
378
542
  export async function runCommand(options, io) {
379
543
  const taskId = requireArg(options, "task");
380
- const run = await executePlanWithBudgetPreflight(taskId, process.cwd(), parseBudgetEscalationDecision(options));
544
+ const run = await executePlanWithBudgetPreflight(taskId, process.cwd(), parseBudgetEscalationDecision(options), removeUndefined({
545
+ estimatedCostUsd: stringOption(options["estimate-cost"])
546
+ ? numberOption(options["estimate-cost"], 0)
547
+ : undefined,
548
+ override: options.yes ? true : undefined,
549
+ }));
381
550
  if (options.json) {
382
551
  io.log(JSON.stringify(run, null, 2));
383
552
  return;
@@ -470,11 +639,14 @@ export async function workflowTemplateSelectCommand(options, io) {
470
639
  }
471
640
  }
472
641
  export async function workflowTemplateRenderCommand(options, io) {
473
- const context = await getTaskContext(requireArg(options, "task"));
642
+ const context = await getTaskContext(requireArg(options, "task"), process.cwd(), {
643
+ tokenBudget: numberOption(options.budget, 2500),
644
+ });
474
645
  const rendered = renderWorkflowTemplates({
475
646
  task: context.task,
476
647
  target: parseSkillRenderTarget(stringOption(options.target) ?? "generic"),
477
648
  templates: context.workflowTemplates,
649
+ contextBudget: context.contextBudget,
478
650
  });
479
651
  if (options.json) {
480
652
  io.log(JSON.stringify(rendered, null, 2));
@@ -489,12 +661,22 @@ export async function workflowRunCommand(options, io) {
489
661
  const taskId = requireArg(options, "task");
490
662
  const gates = (stringOption(options.gates) ?? "phase");
491
663
  const maxIterations = numberOption(options["max-iterations"], 5);
664
+ const timeoutMinutes = numberOption(options["timeout-minutes"], 0);
492
665
  const file = autonomousRunsPath(cwd);
666
+ const phaseSelection = await resolveWorkflowPhaseSelection(cwd, options);
667
+ const config = await getWorkflowConfig(cwd);
668
+ const phaseTimeoutMinutes = config.workflow?.phaseTimeoutMinutes ?? 0;
493
669
  if (!GATE_MODES.includes(gates)) {
494
670
  throw new Error(`--gates must be one of: ${GATE_MODES.join(", ")}`);
495
671
  }
672
+ if (timeoutMinutes < 0) {
673
+ throw new Error("--timeout-minutes must be 0 or greater");
674
+ }
675
+ if (phaseTimeoutMinutes < 0) {
676
+ throw new Error("workflow.phaseTimeoutMinutes must be 0 or greater");
677
+ }
496
678
  if (options["dry-run"]) {
497
- return workflowDryRun(options, io, taskId, gates, maxIterations);
679
+ return workflowDryRun(options, io, taskId, gates, maxIterations, phaseSelection);
498
680
  }
499
681
  let run;
500
682
  let startIndex;
@@ -503,21 +685,39 @@ export async function workflowRunCommand(options, io) {
503
685
  if (!existing)
504
686
  throw new Error(`workflow run not found: ${String(options.resume)}`);
505
687
  run = existing;
506
- startIndex = resumePhaseIndex(run);
688
+ if (run.status === "canceled" || run.status === "failed") {
689
+ throw new Error(`workflow run ${run.id} is ${run.status} and cannot be resumed`);
690
+ }
691
+ const resumeSequence = phaseSequenceForRun(run, phaseSelection.sequence);
692
+ startIndex = resumePhaseIndex(run, resumeSequence);
507
693
  if (startIndex === -1) {
508
694
  io.log(`Workflow ${run.id} is already complete`);
509
695
  if (options.json)
510
696
  io.log(JSON.stringify({ run, file, cwd }, null, 2));
511
697
  return;
512
698
  }
513
- io.log(`Resuming run ${run.id} from phase ${AUTONOMOUS_PHASE_SEQUENCE[startIndex]?.phase}`);
699
+ const pausedGate = run.phases.findLast((phase) => phase.status === "gate_paused");
700
+ if (pausedGate && !pausedGate.approvedBy) {
701
+ io.log(`WARN: gate ${pausedGate.gateId ?? gateIdForPausedPhase(pausedGate.phase, resumeSequence)} has no recorded approval; continuing for backward compatibility`);
702
+ }
703
+ io.log(`Resuming run ${run.id} from phase ${resumeSequence[startIndex]?.phase}`);
704
+ run = { ...run, phaseSequence: resumeSequence.map((phase) => phase.phase) };
514
705
  }
515
706
  else {
516
- run = await createAutonomousRun(cwd, { taskId, gates, maxIterations });
707
+ run = await createAutonomousRun(cwd, {
708
+ taskId,
709
+ gates,
710
+ maxIterations,
711
+ phaseSequence: phaseSelection.sequence.map((phase) => phase.phase),
712
+ skippedPhases: phaseSelection.skipped,
713
+ });
517
714
  startIndex = 0;
518
715
  io.log(`Started autonomous workflow ${run.id} for task ${taskId} [gates=${gates}]`);
519
716
  }
520
- run = await executePhases(cwd, run, startIndex, io);
717
+ run = await executePhases(cwd, run, startIndex, io, phaseSequenceForRun(run, phaseSelection.sequence), {
718
+ runTimeoutMinutes: timeoutMinutes,
719
+ phaseTimeoutMinutes,
720
+ });
521
721
  const result = { run, file, cwd };
522
722
  if (options.json) {
523
723
  io.log(JSON.stringify(result, null, 2));
@@ -535,7 +735,9 @@ export async function workflowRunCommand(options, io) {
535
735
  }
536
736
  export async function workflowRunListCommand(options, io) {
537
737
  const cwd = process.cwd();
538
- const runs = await listAutonomousRuns(cwd);
738
+ const runs = options.active
739
+ ? await listActiveAutonomousRuns(cwd)
740
+ : await listAutonomousRuns(cwd);
539
741
  if (options.json) {
540
742
  io.log(JSON.stringify(runs, null, 2));
541
743
  return;
@@ -551,17 +753,35 @@ export async function workflowRunListCommand(options, io) {
551
753
  io.log(` ${phases}`);
552
754
  }
553
755
  }
554
- const CLARIFICATION_TARGETS = ["po", "architect"];
555
- const CLARIFICATION_ALLOWED_PHASES = new Set(["developer", "qa"]);
756
+ export async function workflowCancelCommand(options, io) {
757
+ const run = await cancelRun(process.cwd(), requireArg(options, "run"), stringOption(options.reason) ?? "Workflow run canceled");
758
+ if (options.json) {
759
+ io.log(JSON.stringify(run, null, 2));
760
+ return;
761
+ }
762
+ io.log(`Workflow canceled [run=${run.id}]`);
763
+ }
764
+ export async function workflowGateApproveCommand(options, io) {
765
+ const approval = await approveWorkflowGate({
766
+ runId: requireArg(options, "run"),
767
+ gateId: requireArg(options, "gate"),
768
+ approver: requireArg(options, "approver"),
769
+ rationale: requireArg(options, "rationale"),
770
+ });
771
+ if (options.json) {
772
+ io.log(JSON.stringify(approval, null, 2));
773
+ return;
774
+ }
775
+ io.log(approval.alreadyApproved
776
+ ? `Gate ${approval.gateId} already approved by ${approval.approver}`
777
+ : `Approved gate ${approval.gateId} for run ${approval.run.id}`);
778
+ }
556
779
  export async function workflowClarifyCommand(options, io) {
557
780
  const cwd = process.cwd();
558
781
  const runId = requireArg(options, "run");
559
- const from = requireArg(options, "from");
560
- const to = requireArg(options, "to");
782
+ const from = await resolveClarificationRole(cwd, requireArg(options, "from"));
783
+ const to = await resolveClarificationRole(cwd, requireArg(options, "to"));
561
784
  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
785
  const run = await readAutonomousRun(cwd, runId);
566
786
  if (!run)
567
787
  throw new Error(`workflow run not found: ${runId}`);
@@ -572,9 +792,6 @@ export async function workflowClarifyCommand(options, io) {
572
792
  throw new Error(`no active phase found for role ${from} in run ${runId}`);
573
793
  }
574
794
  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
795
  // Open clarification record
579
796
  const record = await openClarification(cwd, {
580
797
  runId,
@@ -585,8 +802,9 @@ export async function workflowClarifyCommand(options, io) {
585
802
  });
586
803
  // Suspend the phase if it's still running
587
804
  if (activePhase.status === "running") {
588
- const phaseSeqIdx = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.phase === activePhase.phase);
589
- await suspendPhaseForClarification(cwd, run, phaseSeqIdx);
805
+ const sequence = phaseSequenceForRun(run);
806
+ const phaseSeqIdx = sequence.findIndex((d) => d.phase === activePhase.phase);
807
+ await suspendPhaseForClarification(cwd, run, phaseSeqIdx, sequence);
590
808
  }
591
809
  if (options.json) {
592
810
  io.log(JSON.stringify(record, null, 2));
@@ -610,11 +828,12 @@ export async function workflowClarifyRespondCommand(options, io) {
610
828
  answer,
611
829
  });
612
830
  // Resume the suspended phase back to running
613
- const phaseSeqIdx = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.role === record.fromRole);
831
+ const sequence = phaseSequenceForRun(run);
832
+ const phaseSeqIdx = sequence.findIndex((d) => d.role === record.fromRole);
614
833
  if (phaseSeqIdx !== -1) {
615
834
  const phase = run.phases.find((p) => p.role === record.fromRole);
616
835
  if (phase?.status === "awaiting_clarification") {
617
- await resumePhaseFromClarification(cwd, run, phaseSeqIdx);
836
+ await resumePhaseFromClarification(cwd, run, phaseSeqIdx, sequence);
618
837
  }
619
838
  }
620
839
  if (options.json) {
@@ -628,14 +847,25 @@ export async function workflowClarifyRespondCommand(options, io) {
628
847
  }
629
848
  export async function workflowClarifyListCommand(options, io) {
630
849
  const cwd = process.cwd();
631
- const runId = requireArg(options, "run");
632
- const clarifications = await listClarifications(cwd, runId);
850
+ const runId = stringOption(options.run);
851
+ const taskId = stringOption(options.task);
852
+ if (runId && taskId) {
853
+ throw new Error("workflow clarify-list accepts either --run or --task, not both");
854
+ }
855
+ if (!runId && !taskId) {
856
+ throw new Error("workflow clarify-list requires --run or --task");
857
+ }
858
+ const clarifications = runId
859
+ ? await listClarifications(cwd, runId)
860
+ : await listClarificationsByTask(cwd, taskId);
633
861
  if (options.json) {
634
862
  io.log(JSON.stringify(clarifications, null, 2));
635
863
  return;
636
864
  }
637
865
  if (clarifications.length === 0) {
638
- io.log(`No clarifications for run ${runId}`);
866
+ io.log(runId
867
+ ? `No clarifications for run ${runId}`
868
+ : `No clarifications for task ${taskId}`);
639
869
  return;
640
870
  }
641
871
  for (const c of clarifications) {
@@ -646,17 +876,115 @@ export async function workflowClarifyListCommand(options, io) {
646
876
  io.log(` A: ${c.answer}`);
647
877
  }
648
878
  }
649
- const DEV_PHASE_INDEX = AUTONOMOUS_PHASE_SEQUENCE.findIndex((d) => d.phase === "developer");
650
- async function executePhases(cwd, run, startIndex, io) {
879
+ const MEMORY_HOOK_POINTS = [
880
+ "before_plan",
881
+ "before_implementation",
882
+ "before_handoff",
883
+ "before_final",
884
+ "after_failure",
885
+ ];
886
+ function parseMemoryHook(value) {
887
+ const hook = value ?? "before_implementation";
888
+ if (!MEMORY_HOOK_POINTS.includes(hook)) {
889
+ throw new Error(`memory hook must be one of: ${MEMORY_HOOK_POINTS.join(", ")}`);
890
+ }
891
+ return hook;
892
+ }
893
+ async function resolveWorkflowPhaseSelection(root, options) {
894
+ const config = await getWorkflowConfig(root);
895
+ const configuredIds = config.workflow?.phaseSequence;
896
+ const baseSequence = configuredIds
897
+ ? phaseSequenceFromIds(configuredIds, "workflow.phaseSequence")
898
+ : AUTONOMOUS_PHASE_SEQUENCE;
899
+ const skipIds = parseCsv(options.skip);
900
+ validatePhaseIds(skipIds, "--skip");
901
+ const skipSet = new Set(skipIds);
902
+ const sequence = baseSequence.filter((phase) => !skipSet.has(phase.phase));
903
+ validateWorkflowPhaseSequence(sequence);
904
+ const sequenceIds = new Set(sequence.map((phase) => phase.phase));
905
+ const skipped = AUTONOMOUS_PHASE_SEQUENCE.filter((phase) => !sequenceIds.has(phase.phase));
906
+ return { sequence, skipped };
907
+ }
908
+ function phaseSequenceForRun(run, fallback = AUTONOMOUS_PHASE_SEQUENCE) {
909
+ return run.phaseSequence
910
+ ? phaseSequenceFromIds(run.phaseSequence, "run.phaseSequence")
911
+ : fallback;
912
+ }
913
+ function phaseSequenceFromIds(ids, source) {
914
+ validatePhaseIds(ids, source);
915
+ const byId = new Map(AUTONOMOUS_PHASE_SEQUENCE.map((phase) => [phase.phase, phase]));
916
+ return ids.map((id) => byId.get(id));
917
+ }
918
+ function validatePhaseIds(ids, source) {
919
+ const valid = new Set(AUTONOMOUS_PHASE_SEQUENCE.map((phase) => phase.phase));
920
+ const unknown = ids.filter((id) => !valid.has(id));
921
+ if (unknown.length > 0) {
922
+ throw new Error(`${source} contains invalid phase(s): ${unknown.join(", ")}. Valid phases: ${[...valid].join(", ")}`);
923
+ }
924
+ }
925
+ function validateWorkflowPhaseSequence(sequence) {
926
+ if (sequence.length === 0) {
927
+ throw new Error("workflow phase sequence cannot be empty");
928
+ }
929
+ const ids = sequence.map((phase) => phase.phase);
930
+ const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
931
+ if (duplicates.length > 0) {
932
+ throw new Error(`workflow phase sequence contains duplicate phase(s): ${[...new Set(duplicates)].join(", ")}`);
933
+ }
934
+ const qaIndex = ids.indexOf("qa");
935
+ const developerIndex = ids.indexOf("developer");
936
+ if (qaIndex !== -1 && developerIndex === -1) {
937
+ throw new Error("workflow phase sequence with qa must include developer");
938
+ }
939
+ if (qaIndex !== -1 && developerIndex > qaIndex) {
940
+ throw new Error("workflow phase sequence must place developer before qa");
941
+ }
942
+ }
943
+ function phaseExceededTimeout(run, phaseId, timeoutMs) {
944
+ const phase = run.phases.find((candidate) => candidate.phase === phaseId &&
945
+ (candidate.status === "running" ||
946
+ candidate.status === "awaiting_clarification"));
947
+ if (!phase) {
948
+ return false;
949
+ }
950
+ return Date.now() - new Date(phase.startedAt).getTime() > timeoutMs;
951
+ }
952
+ async function recordWorkflowTimeout(root, run, input) {
953
+ await appendEvent(root, {
954
+ type: "WORKFLOW_TIMEOUT",
955
+ taskId: run.taskId,
956
+ actor: "parent",
957
+ summary: input.reason,
958
+ metadata: {
959
+ runId: run.id,
960
+ phase: input.phase,
961
+ timeoutMinutes: input.timeoutMinutes,
962
+ },
963
+ });
964
+ }
965
+ function gateIdForPausedPhase(phase, sequence = AUTONOMOUS_PHASE_SEQUENCE) {
966
+ const sequenceIndex = sequence.findIndex((candidate) => candidate.phase === phase);
967
+ const next = sequence[sequenceIndex + 1]?.phase ?? "end";
968
+ return `${phase}->${next}`;
969
+ }
970
+ async function executePhases(cwd, run, startIndex, io, sequence, timeoutOptions) {
651
971
  let current = run;
652
972
  let qaFailNotes;
653
- for (let i = startIndex; i < AUTONOMOUS_PHASE_SEQUENCE.length; i++) {
654
- const def = AUTONOMOUS_PHASE_SEQUENCE[i];
973
+ const devPhaseIndex = sequence.findIndex((d) => d.phase === "developer");
974
+ const runDeadlineMs = timeoutOptions.runTimeoutMinutes > 0
975
+ ? new Date(run.createdAt).getTime() +
976
+ timeoutOptions.runTimeoutMinutes * 60 * 1000
977
+ : undefined;
978
+ const phaseTimeoutMs = timeoutOptions.phaseTimeoutMinutes > 0
979
+ ? timeoutOptions.phaseTimeoutMinutes * 60 * 1000
980
+ : undefined;
981
+ for (let i = startIndex; i < sequence.length; i++) {
982
+ const def = sequence[i];
655
983
  // Init phase task (skip if already has a running/done/clarification record from resume)
656
984
  const existing = current.phases.find((p) => p.phase === def.phase && p.status !== "qa_failed");
657
985
  if (!existing || existing.status === "pending") {
658
986
  const retryContext = def.phase === "developer" && qaFailNotes ? qaFailNotes : undefined;
659
- const phaseRecord = await initPhase(cwd, current, i, retryContext);
987
+ const phaseRecord = await initPhase(cwd, current, i, retryContext, sequence);
660
988
  current = { ...current, phases: [...current.phases, phaseRecord] };
661
989
  if (retryContext) {
662
990
  io.log(`↺ ${def.phase} (${def.role}) retry — QA findings: ${retryContext.slice(0, 80)}`);
@@ -682,7 +1010,7 @@ async function executePhases(cwd, run, startIndex, io) {
682
1010
  }).catch(async (error) => {
683
1011
  const message = error instanceof Error ? error.message : String(error);
684
1012
  io.log(`✗ ${def.phase} provider execution failed: ${message}`);
685
- current = await markRunFailed(cwd, current, message);
1013
+ current = await markRunFailed(cwd, current, message, def.phase);
686
1014
  return null;
687
1015
  });
688
1016
  if (!llmResult)
@@ -690,6 +1018,17 @@ async function executePhases(cwd, run, startIndex, io) {
690
1018
  if (llmResult.mode === "llm") {
691
1019
  io.log(` ✓ llm artifact (${llmResult.artifact})`);
692
1020
  }
1021
+ if (phaseTimeoutMs !== undefined &&
1022
+ phaseExceededTimeout(current, def.phase, phaseTimeoutMs)) {
1023
+ io.log(`✗ ${def.phase} phase timeout`);
1024
+ await recordWorkflowTimeout(cwd, current, {
1025
+ phase: def.phase,
1026
+ reason: "phase timeout",
1027
+ timeoutMinutes: timeoutOptions.phaseTimeoutMinutes,
1028
+ });
1029
+ current = await markRunFailed(cwd, current, "phase timeout", def.phase);
1030
+ return current;
1031
+ }
693
1032
  // Architect sizing gate — always enforced regardless of --gates mode.
694
1033
  // In LLM mode, the architect phase records the sizing decision before this check.
695
1034
  if (def.phase === "architect") {
@@ -705,15 +1044,15 @@ async function executePhases(cwd, run, startIndex, io) {
705
1044
  io.log(` ✓ sizing=${sizing.sizing}${sizing.points !== undefined ? ` (${sizing.points} pts)` : ""}`);
706
1045
  }
707
1046
  const outcome = llmResult.outcome;
708
- const result = await closePhase(cwd, current, i, outcome);
1047
+ const result = await closePhase(cwd, current, i, outcome, sequence);
709
1048
  current = result.run;
710
1049
  if (result.handoffArtifact) {
711
- io.log(` ✓ handoff → ${AUTONOMOUS_PHASE_SEQUENCE[i + 1]?.role ?? "end"} (${result.handoffArtifact})`);
1050
+ io.log(` ✓ handoff → ${sequence[i + 1]?.role ?? "end"} (${result.handoffArtifact})`);
712
1051
  }
713
1052
  if (result.reviewArtifact) {
714
- const nextPhase = AUTONOMOUS_PHASE_SEQUENCE[i + 1]?.phase;
1053
+ const nextPhase = sequence[i + 1]?.phase;
715
1054
  io.log(` ⏸ gate ${def.phase}→${nextPhase} — review: ${result.reviewArtifact}`);
716
- io.log(` Approve: orchestra workflow run --task ${current.taskId} --resume ${current.id}`);
1055
+ io.log(` Approve: orchestra workflow gate-approve --run ${current.id} --gate ${def.phase}->${nextPhase} --approver <name> --rationale "<text>"`);
717
1056
  io.log(``);
718
1057
  io.log(`╔══ GATE PAUSE: ${def.phase.toUpperCase()} → ${nextPhase?.toUpperCase()} ══════════════════════`);
719
1058
  io.log(`║ Run: ${current.id}`);
@@ -721,6 +1060,8 @@ async function executePhases(cwd, run, startIndex, io) {
721
1060
  io.log(`║ Review: ${result.reviewArtifact}`);
722
1061
  io.log(`║`);
723
1062
  io.log(`║ To approve, run:`);
1063
+ io.log(`║ orchestra workflow gate-approve --run ${current.id} --gate ${def.phase}->${nextPhase} --approver <name> --rationale "<text>"`);
1064
+ io.log(`║ Then resume:`);
724
1065
  io.log(`║ orchestra workflow run --task ${current.taskId} --resume ${current.id}`);
725
1066
  io.log(`╚══════════════════════════════════════════════════════════════`);
726
1067
  return current;
@@ -730,7 +1071,7 @@ async function executePhases(cwd, run, startIndex, io) {
730
1071
  if (closedPhase) {
731
1072
  qaFailNotes = closedPhase.notes ?? "QA findings — see QA phase artifact";
732
1073
  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
1074
+ i = devPhaseIndex - 1; // will be incremented to developer at top of loop
734
1075
  continue;
735
1076
  }
736
1077
  // Release phase: auto-create PR if configured
@@ -761,18 +1102,29 @@ async function executePhases(cwd, run, startIndex, io) {
761
1102
  }
762
1103
  }
763
1104
  }
1105
+ if (runDeadlineMs !== undefined && Date.now() > runDeadlineMs) {
1106
+ const reason = `wall-clock timeout after ${timeoutOptions.runTimeoutMinutes} minutes`;
1107
+ io.log(`✗ ${reason}`);
1108
+ await recordWorkflowTimeout(cwd, current, {
1109
+ phase: def.phase,
1110
+ reason,
1111
+ timeoutMinutes: timeoutOptions.runTimeoutMinutes,
1112
+ });
1113
+ current = await markRunFailed(cwd, current, reason);
1114
+ return current;
1115
+ }
764
1116
  }
765
1117
  current = await markRunDone(cwd, current);
766
1118
  return current;
767
1119
  }
768
- async function workflowDryRun(options, io, taskId, gates, maxIterations) {
1120
+ async function workflowDryRun(options, io, taskId, gates, maxIterations, phaseSelection) {
769
1121
  const gateTransitions = new Set(["po→architect", "qa→release"]);
770
1122
  io.log(`Dry run — no records will be created`);
771
1123
  io.log(`Task: ${taskId} gates: ${gates} max-iterations: ${maxIterations}`);
772
1124
  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];
1125
+ for (let i = 0; i < phaseSelection.sequence.length; i++) {
1126
+ const def = phaseSelection.sequence[i];
1127
+ const next = phaseSelection.sequence[i + 1];
776
1128
  const transitionKey = next ? `${def.phase}→${next.phase}` : "";
777
1129
  const gateLabel = gates === "all"
778
1130
  ? "gate=yes"
@@ -781,13 +1133,18 @@ async function workflowDryRun(options, io, taskId, gates, maxIterations) {
781
1133
  : "gate=no";
782
1134
  io.log(` ${def.phase} (${def.role}) ${gateLabel}`);
783
1135
  }
1136
+ if (phaseSelection.skipped.length > 0) {
1137
+ io.log(``);
1138
+ io.log(`Skipped: ${phaseSelection.skipped.map((phase) => phase.phase).join(", ")}`);
1139
+ }
784
1140
  if (options.json) {
785
1141
  io.log(JSON.stringify({
786
1142
  dryRun: true,
787
1143
  taskId,
788
1144
  gates,
789
1145
  maxIterations,
790
- phases: AUTONOMOUS_PHASE_SEQUENCE,
1146
+ phases: phaseSelection.sequence,
1147
+ skipped: phaseSelection.skipped,
791
1148
  }, null, 2));
792
1149
  }
793
1150
  }
@@ -810,6 +1167,16 @@ function parseCsv(value) {
810
1167
  .map((item) => item.trim())
811
1168
  .filter(Boolean);
812
1169
  }
1170
+ function csvOption(value) {
1171
+ if (typeof value !== "string") {
1172
+ return undefined;
1173
+ }
1174
+ return parseCsv(value);
1175
+ }
1176
+ function singleValueArrayOption(value) {
1177
+ const option = stringOption(value);
1178
+ return option ? [option] : undefined;
1179
+ }
813
1180
  function stringOption(value) {
814
1181
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
815
1182
  }
@@ -914,6 +1281,24 @@ function renderDependencyReport(report) {
914
1281
  : report.dependencies.map((dependency) => `- ${dependency.id}: ${dependency.status} (${dependency.isComplete ? "complete" : "incomplete"})`)),
915
1282
  ].join("\n");
916
1283
  }
1284
+ function renderPreRunValidation(report) {
1285
+ const status = report.allowed ? "allowed" : "blocked";
1286
+ const missing = report.missing.length > 0 ? report.missing.join(", ") : "none";
1287
+ return [
1288
+ `Pre-run validation for ${report.taskId}: ${status}`,
1289
+ `- Ready: ${String(report.isReady)}`,
1290
+ `- Bypassed: ${String(report.isBypassed)}`,
1291
+ `- Missing: ${missing}`,
1292
+ `- Task: ${String(report.checks.task)}`,
1293
+ `- Estimate: ${String(report.checks.estimate)}`,
1294
+ `- Workflow run: ${String(report.checks.workflowRun)}`,
1295
+ `- Evidence: ${String(report.checks.evidence)}`,
1296
+ `- Review: ${String(report.checks.review)}`,
1297
+ ...(report.bypassDecisionArtifact
1298
+ ? [`- Bypass decision: ${report.bypassDecisionArtifact}`]
1299
+ : []),
1300
+ ].join("\n");
1301
+ }
917
1302
  function renderTaskGraphPlan(plan) {
918
1303
  return [
919
1304
  "Task graph plan",
@@ -931,6 +1316,28 @@ function renderTaskGraphPlan(plan) {
931
1316
  ...taskGraphReadyLines(plan.complete),
932
1317
  ].join("\n");
933
1318
  }
1319
+ function renderTaskGraphDryRunPreview(preview) {
1320
+ return [
1321
+ "Task graph dry run",
1322
+ `- Mode: ${preview.mode}`,
1323
+ `- Would mutate: ${String(preview.wouldMutate)}`,
1324
+ `- Selected: ${preview.selectedTaskIds.join(", ")}`,
1325
+ "",
1326
+ "Tasks:",
1327
+ ...preview.tasks.map((task) => {
1328
+ const budget = task.budget
1329
+ ? ` budget=${task.budget.passed ? "pass" : "fail"} scopes=${task.budget.appliedBudgets.join(",") || "none"}`
1330
+ : " budget=not-configured";
1331
+ return `- ${task.id} ${task.title} (${task.ownerRole}) ${task.provider}/${task.model} fallbacks=${task.fallbacks.join(",") || "none"}${budget}`;
1332
+ }),
1333
+ "",
1334
+ "Locked:",
1335
+ ...taskGraphLockedLines(preview.locked),
1336
+ "",
1337
+ "Blocked:",
1338
+ ...taskGraphBlockedLines(preview.blocked),
1339
+ ].join("\n");
1340
+ }
934
1341
  function renderTaskGraphBatchRun(batch) {
935
1342
  return [
936
1343
  "Task graph batch run",
@@ -1097,6 +1504,11 @@ function renderTaskContextMarkdown(context) {
1097
1504
  `- Locks: ${context.locks.length}`,
1098
1505
  `- Risks: ${context.risks.length}`,
1099
1506
  `- Delegation: ${context.delegation?.recommendation ?? "not decided"}`,
1507
+ `- Estimated tokens: ${context.contextBudget.estimatedTokens}/${context.contextBudget.tokenBudget}`,
1508
+ `- Trimmed: ${context.contextBudget.trimmed}`,
1509
+ "",
1510
+ "## Context Budget",
1511
+ ...context.contextBudget.sections.map((section) => `- ${section.section}: kept ${section.keptCount}/${section.originalCount}, dropped ${section.droppedCount}`),
1100
1512
  "",
1101
1513
  "## Collaboration Flow",
1102
1514
  ...(context.collaborationFlow
@@ -1118,6 +1530,8 @@ function renderTaskContextMarkdown(context) {
1118
1530
  : context.skills.selected.map((item) => `- ${item.skill.id} (score ${item.score}): ${item.rationale.join("; ")}`)),
1119
1531
  `- Source groups: ${context.skills.sourceGroups.join(", ") || "none"}`,
1120
1532
  "",
1533
+ renderMemoryPacket(context.memory),
1534
+ "",
1121
1535
  "## Decisions",
1122
1536
  ...eventLines(context.decisions),
1123
1537
  "",
@@ -1190,6 +1604,27 @@ function renderPlaywrightPlanMarkdown(plan) {
1190
1604
  "",
1191
1605
  ].join("\n");
1192
1606
  }
1607
+ function renderTaskDetails(task) {
1608
+ return [
1609
+ `ID: ${task.id}`,
1610
+ `Title: ${task.title}`,
1611
+ `Owner: ${task.ownerRole}`,
1612
+ `Status: ${task.status}`,
1613
+ `Goal: ${task.goal ?? "—"}`,
1614
+ `Scope: ${task.scope ?? "—"}`,
1615
+ `Acceptance Criteria: ${renderInlineList(task.acceptanceCriteria)}`,
1616
+ `Assumptions: ${renderInlineList(task.assumptions)}`,
1617
+ `Risks: ${renderInlineList(task.risks)}`,
1618
+ `Paths: ${renderInlineList(task.paths)}`,
1619
+ `Test Strategy: ${task.testStrategy ?? "—"}`,
1620
+ `Blocked Reason: ${task.blockedReason ?? "—"}`,
1621
+ `Created: ${task.createdAt}`,
1622
+ `Updated: ${task.updatedAt}`,
1623
+ ];
1624
+ }
1625
+ function renderInlineList(values) {
1626
+ return values && values.length > 0 ? values.join(", ") : "—";
1627
+ }
1193
1628
  function eventLines(events) {
1194
1629
  return events.length === 0
1195
1630
  ? ["- none"]