@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
@@ -11,7 +11,14 @@ import { analyzePullRequestReview } from "./pr-review.js";
11
11
  import { planSkillsForTask } from "./skills.js";
12
12
  import { getTelemetryConsent } from "./telemetry.js";
13
13
  import { latestDelegationDecision } from "./delegation-decision.js";
14
+ import { listAutonomousRuns } from "./autonomous-workflow.js";
15
+ import { persistRun, readAutonomousRun } from "./autonomous-run-store.js";
16
+ import { AUTONOMOUS_PHASE_SEQUENCE } from "./autonomous-workflow-constants.js";
17
+ import { readEstimate } from "./benchmark.js";
18
+ import { notifyWorkflowLifecycle, } from "./notifications.js";
14
19
  import { handoffFlowRequirements, recommendCollaborationFlow, } from "./collaboration-flows.js";
20
+ import { queryMemory, recordMemoryEvent } from "./memory.js";
21
+ import { applyContextBudget, DEFAULT_CONTEXT_TOKEN_BUDGET, } from "./context-budget.js";
15
22
  import { selectWorkflowTemplates } from "./workflow-templates.js";
16
23
  import { SIZING_LABELS } from "./types.js";
17
24
  export async function getWorkflowStatus(root = process.cwd()) {
@@ -70,13 +77,129 @@ export async function listTasks(root = process.cwd()) {
70
77
  const workspace = await loadWorkspace(root);
71
78
  return workspace.tasks;
72
79
  }
80
+ export async function deleteTask(taskId, options = {}, root = process.cwd()) {
81
+ const workspace = await loadWorkspace(root);
82
+ let deleted;
83
+ await mutateTasks(workspace.base, (tasks) => {
84
+ const taskIndex = tasks.findIndex((task) => task.id === taskId);
85
+ if (taskIndex < 0) {
86
+ throw new Error(`unknown task: ${taskId}`);
87
+ }
88
+ const current = tasks[taskIndex];
89
+ if (!current) {
90
+ throw new Error(`unknown task: ${taskId}`);
91
+ }
92
+ assertTaskCanBeRemoved(current, tasks, options.force ?? false);
93
+ deleted = current;
94
+ return tasks.filter((task) => task.id !== taskId);
95
+ });
96
+ if (!deleted) {
97
+ throw new Error(`unknown task: ${taskId}`);
98
+ }
99
+ await appendEvent(root, {
100
+ type: "TASK_DELETED",
101
+ taskId,
102
+ actor: "parent",
103
+ summary: `Task deleted: ${taskId}`,
104
+ metadata: {
105
+ title: deleted.title,
106
+ status: deleted.status,
107
+ forced: Boolean(options.force),
108
+ },
109
+ });
110
+ return deleted;
111
+ }
112
+ export async function archiveTask(taskId, options = {}, root = process.cwd()) {
113
+ const workspace = await loadWorkspace(root);
114
+ let archived;
115
+ await mutateTasks(workspace.base, (tasks) => {
116
+ const taskIndex = tasks.findIndex((task) => task.id === taskId);
117
+ if (taskIndex < 0) {
118
+ throw new Error(`unknown task: ${taskId}`);
119
+ }
120
+ const current = tasks[taskIndex];
121
+ if (!current) {
122
+ throw new Error(`unknown task: ${taskId}`);
123
+ }
124
+ assertTaskCanBeRemoved(current, tasks, options.force ?? false);
125
+ archived = {
126
+ ...current,
127
+ status: "archived",
128
+ updatedAt: new Date().toISOString(),
129
+ };
130
+ const nextTasks = [...tasks];
131
+ nextTasks[taskIndex] = archived;
132
+ return nextTasks;
133
+ });
134
+ if (!archived) {
135
+ throw new Error(`unknown task: ${taskId}`);
136
+ }
137
+ await appendEvent(root, {
138
+ type: "TASK_ARCHIVED",
139
+ taskId,
140
+ actor: "parent",
141
+ summary: `Task archived: ${taskId}`,
142
+ metadata: {
143
+ title: archived.title,
144
+ forced: Boolean(options.force),
145
+ },
146
+ });
147
+ return archived;
148
+ }
73
149
  export async function listRoles(root = process.cwd()) {
74
150
  const workspace = await loadWorkspace(root);
75
151
  return workspace.roles;
76
152
  }
153
+ export async function validatePreRun(taskId, options = {}, root = process.cwd()) {
154
+ const workspace = await loadWorkspace(root);
155
+ const task = workspace.tasks.find((candidate) => candidate.id === taskId);
156
+ const events = await readEvents(root);
157
+ const estimate = task ? await readEstimate(root, taskId) : undefined;
158
+ const runs = task ? await listAutonomousRuns(root) : [];
159
+ const checks = {
160
+ task: Boolean(task),
161
+ estimate: Boolean(estimate),
162
+ workflowRun: runs.some((run) => run.taskId === taskId),
163
+ evidence: events.some((event) => event.taskId === taskId && event.type === "EVIDENCE_ADDED"),
164
+ review: events.some((event) => event.taskId === taskId && event.type === "REVIEW_RECORDED"),
165
+ };
166
+ const missing = Object.entries(checks)
167
+ .filter(([, passed]) => !passed)
168
+ .map(([name]) => name);
169
+ const isReady = missing.length === 0;
170
+ let bypassDecisionArtifact;
171
+ if (!isReady && options.bypass && checks.task) {
172
+ const owner = options.bypassOwner ?? "architect";
173
+ const rationale = options.bypassRationale ?? "User accepted pre-run validation bypass.";
174
+ const decision = await recordDecision({
175
+ task: taskId,
176
+ owner,
177
+ title: "Pre-run validation bypass",
178
+ context: `Missing checks: ${missing.join(", ")}`,
179
+ decision: "Proceed despite incomplete proactive workflow checks.",
180
+ consequences: rationale,
181
+ status: "accepted",
182
+ }, root);
183
+ bypassDecisionArtifact = decision.artifact;
184
+ }
185
+ const isBypassed = Boolean(bypassDecisionArtifact);
186
+ return removeUndefined({
187
+ taskId,
188
+ isReady,
189
+ isBypassed,
190
+ allowed: isReady || isBypassed,
191
+ missing,
192
+ checks,
193
+ bypassDecisionArtifact,
194
+ });
195
+ }
77
196
  export async function updateTask(input, root = process.cwd()) {
78
197
  const workspace = await loadWorkspace(root);
198
+ if (input.ownerRole && !workspace.roleIds.has(input.ownerRole)) {
199
+ throw new Error(`unknown owner role: ${input.ownerRole}`);
200
+ }
79
201
  let updated;
202
+ let changedFields = [];
80
203
  await mutateTasks(workspace.base, (tasks) => {
81
204
  const taskIndex = tasks.findIndex((task) => task.id === input.id);
82
205
  if (taskIndex < 0) {
@@ -86,12 +209,11 @@ export async function updateTask(input, root = process.cwd()) {
86
209
  if (!current) {
87
210
  throw new Error(`unknown task: ${input.id}`);
88
211
  }
89
- updated = removeUndefined({
90
- ...current,
91
- status: input.status,
92
- blockedReason: input.blockedReason,
93
- updatedAt: new Date().toISOString(),
94
- });
212
+ const next = { ...current };
213
+ const changes = new Set();
214
+ applyTaskUpdate(next, current, input, changes);
215
+ changedFields = [...changes].sort();
216
+ updated = { ...next, updatedAt: new Date().toISOString() };
95
217
  const nextTasks = [...tasks];
96
218
  nextTasks[taskIndex] = updated;
97
219
  return nextTasks;
@@ -104,10 +226,39 @@ export async function updateTask(input, root = process.cwd()) {
104
226
  taskId: input.id,
105
227
  actor: "parent",
106
228
  summary: `Task updated: ${input.id}`,
107
- metadata: { status: updated.status },
229
+ metadata: { status: updated.status, changedFields },
108
230
  });
109
231
  return updated;
110
232
  }
233
+ function applyTaskUpdate(next, current, input, changedFields) {
234
+ setTaskField(next, current, "title", input.title, changedFields);
235
+ setTaskField(next, current, "ownerRole", input.ownerRole, changedFields);
236
+ setTaskField(next, current, "goal", input.goal, changedFields);
237
+ setTaskField(next, current, "scope", input.scope, changedFields);
238
+ setTaskField(next, current, "paths", input.paths, changedFields);
239
+ setTaskField(next, current, "testStrategy", input.testStrategy, changedFields);
240
+ setTaskField(next, current, "status", input.status, changedFields);
241
+ setTaskField(next, current, "blockedReason", input.blockedReason, changedFields);
242
+ appendTaskField(next, "acceptanceCriteria", input.acceptanceCriteria, changedFields);
243
+ appendTaskField(next, "assumptions", input.assumptions, changedFields);
244
+ appendTaskField(next, "risks", input.risks, changedFields);
245
+ }
246
+ function setTaskField(next, current, key, value, changedFields) {
247
+ if (value === undefined) {
248
+ return;
249
+ }
250
+ if (JSON.stringify(current[key]) !== JSON.stringify(value)) {
251
+ changedFields.add(String(key));
252
+ }
253
+ next[key] = value;
254
+ }
255
+ function appendTaskField(next, key, values, changedFields) {
256
+ if (!values || values.length === 0) {
257
+ return;
258
+ }
259
+ next[key] = [...(next[key] ?? []), ...values];
260
+ changedFields.add(key);
261
+ }
111
262
  export async function checkTaskDependencies(taskId, root = process.cwd()) {
112
263
  const workspace = await loadWorkspace(root);
113
264
  const task = workspace.tasks.find((candidate) => candidate.id === taskId);
@@ -184,6 +335,35 @@ export async function executeNextReadyTask(root = process.cwd()) {
184
335
  }
185
336
  return executePlanWithBudgetPreflight(next.id, root);
186
337
  }
338
+ export async function previewTaskGraphRun(mode, root = process.cwd()) {
339
+ const plan = await generateTaskGraphPlan(root);
340
+ const selected = mode === "run-next" ? plan.ready.slice(0, 1) : plan.ready;
341
+ if (selected.length === 0) {
342
+ throw new Error("no ready tasks to run");
343
+ }
344
+ const config = await getWorkflowConfig(root);
345
+ const hasBudgets = hasConfiguredBudgets(config.budgets);
346
+ const tasks = [];
347
+ for (const task of selected) {
348
+ const routing = config.providers.byRole[task.ownerRole] ?? config.providers.defaults;
349
+ tasks.push({
350
+ ...task,
351
+ provider: routing.provider,
352
+ model: routing.model,
353
+ fallbacks: routing.fallbacks,
354
+ ...(hasBudgets ? { budget: await checkUsageBudget(task.id, root) } : {}),
355
+ });
356
+ }
357
+ return {
358
+ mode,
359
+ dryRun: true,
360
+ wouldMutate: false,
361
+ selectedTaskIds: tasks.map((task) => task.id),
362
+ tasks,
363
+ locked: plan.locked,
364
+ blocked: plan.blocked,
365
+ };
366
+ }
187
367
  export async function executeReadyTaskBatch(root = process.cwd()) {
188
368
  const plan = await generateTaskGraphPlan(root);
189
369
  const selectedTaskIds = plan.ready.map((task) => task.id);
@@ -210,6 +390,22 @@ export async function executeReadyTaskBatch(root = process.cwd()) {
210
390
  }
211
391
  return recordTaskGraphBatchRun({ selectedTaskIds, runs, locked: plan.locked }, root);
212
392
  }
393
+ function hasConfiguredBudgets(budgets) {
394
+ if (!budgets) {
395
+ return false;
396
+ }
397
+ return (hasBudgetLimits(budgets.defaults) ||
398
+ Object.values(budgets.byRole ?? {}).some(hasBudgetLimits) ||
399
+ Object.values(budgets.byTask ?? {}).some(hasBudgetLimits));
400
+ }
401
+ function hasBudgetLimits(limit) {
402
+ return Boolean(limit &&
403
+ (limit.maxRequests !== undefined ||
404
+ limit.maxInputTokens !== undefined ||
405
+ limit.maxOutputTokens !== undefined ||
406
+ limit.maxTotalTokens !== undefined ||
407
+ limit.maxEstimatedCostUsd !== undefined));
408
+ }
213
409
  export async function listLocks(root = process.cwd()) {
214
410
  const workspace = await loadWorkspace(root);
215
411
  return workspace.locks;
@@ -322,6 +518,10 @@ export async function createHandoff(input, root = process.cwd()) {
322
518
  if (!workspace.roleIds.has(input.from) || !workspace.roleIds.has(input.to)) {
323
519
  throw new Error("handoff roles must exist in roles.json");
324
520
  }
521
+ if (input.updateOwner &&
522
+ !workspace.tasks.some((task) => task.id === input.task)) {
523
+ throw new Error(`unknown task: ${input.task}`);
524
+ }
325
525
  const fileName = `${input.task}-${input.from}-to-${input.to}.md`;
326
526
  const content = [
327
527
  `# Handoff ${input.task}: ${input.from} to ${input.to}`,
@@ -346,7 +546,11 @@ export async function createHandoff(input, root = process.cwd()) {
346
546
  actor: input.from,
347
547
  summary: `Handoff ready for ${input.to}`,
348
548
  artifacts: [artifact],
549
+ metadata: { to: input.to, updateOwner: Boolean(input.updateOwner) },
349
550
  });
551
+ if (input.updateOwner) {
552
+ await updateTask({ id: input.task, ownerRole: input.to }, root);
553
+ }
350
554
  return { artifact, content };
351
555
  }
352
556
  export async function recordDecision(input, root = process.cwd()) {
@@ -408,7 +612,7 @@ export async function recordDecision(input, root = process.cwd()) {
408
612
  export async function listDecisions(taskId, root = process.cwd()) {
409
613
  return filterEvents("DECISION_RECORDED", taskId, root);
410
614
  }
411
- export async function getTaskContext(taskId, root = process.cwd()) {
615
+ export async function getTaskContext(taskId, root = process.cwd(), options = {}) {
412
616
  const workspace = await loadWorkspace(root);
413
617
  const task = workspace.tasks.find((candidate) => candidate.id === taskId);
414
618
  if (!task) {
@@ -416,7 +620,13 @@ export async function getTaskContext(taskId, root = process.cwd()) {
416
620
  }
417
621
  const events = await readEvents(root);
418
622
  const taskEvents = events.filter((event) => event.taskId === taskId);
419
- return removeUndefined({
623
+ const memory = await queryMemory({
624
+ taskId,
625
+ hook: "before_implementation",
626
+ root,
627
+ });
628
+ await recordMemoryEvent(root, memory, "MEMORY_QUERIED");
629
+ const context = removeUndefined({
420
630
  task,
421
631
  dependencies: await checkTaskDependencies(taskId, root),
422
632
  locks: workspace.locks.filter((lock) => lock.taskId === taskId),
@@ -427,12 +637,14 @@ export async function getTaskContext(taskId, root = process.cwd()) {
427
637
  gates: taskEvents.filter((event) => event.type === "GATE_PASSED" || event.type === "GATE_BLOCKED"),
428
638
  modelProvenance: await listModelProvenance(taskId, root),
429
639
  skills: await planSkillsForTask(taskId, root),
640
+ memory,
430
641
  telemetry: await getTelemetryConsent(root),
431
642
  risks: task.risks ?? [],
432
643
  delegation: latestDelegationDecision(taskEvents),
433
644
  collaborationFlow: recommendCollaborationFlow(task, taskEvents),
434
645
  workflowTemplates: selectWorkflowTemplates(task, taskEvents),
435
646
  });
647
+ return applyContextBudget(context, options.tokenBudget ?? DEFAULT_CONTEXT_TOKEN_BUDGET);
436
648
  }
437
649
  export async function generateExecutionPlan(taskId, root = process.cwd()) {
438
650
  const context = await getTaskContext(taskId, root);
@@ -575,9 +787,12 @@ export async function executePlanWithFakeProvider(taskId, root = process.cwd(),
575
787
  budgetEscalation,
576
788
  });
577
789
  }
578
- export async function executePlanWithBudgetPreflight(taskId, root = process.cwd(), decision) {
790
+ export async function executePlanWithBudgetPreflight(taskId, root = process.cwd(), decision, estimate) {
579
791
  await enforceLockPreflight(taskId, root);
580
792
  await enforceDependencyPreflight(taskId, root);
793
+ if (estimate?.estimatedCostUsd !== undefined) {
794
+ await enforceBudgetEstimatePreflight(taskId, root, estimate.estimatedCostUsd, Boolean(estimate.override));
795
+ }
581
796
  const budget = await checkUsageBudget(taskId, root);
582
797
  if (budget.passed) {
583
798
  return executePlanWithFakeProvider(taskId, root);
@@ -664,6 +879,27 @@ async function executeApprovedBudgetFallback(taskId, root, proposal, approval) {
664
879
  }),
665
880
  });
666
881
  }
882
+ export async function checkBudgetEstimatePreflight(taskId, estimatedCostUsd, root = process.cwd()) {
883
+ if (!Number.isFinite(estimatedCostUsd) || estimatedCostUsd < 0) {
884
+ throw new Error("estimated cost must be a non-negative number");
885
+ }
886
+ const config = await readJson(resolveWorkflowPath(root, FILES.config), {});
887
+ const workspace = await loadWorkspace(root);
888
+ const task = workspace.tasks.find((candidate) => candidate.id === taskId);
889
+ if (!task) {
890
+ throw new Error(`unknown task: ${taskId}`);
891
+ }
892
+ const usage = await getUsageReport(taskId, root);
893
+ const projectedUsage = projectUsageCost(usage, task.ownerRole, estimatedCostUsd);
894
+ const budget = await checkUsageBudgetForUsage(taskId, task.ownerRole, projectedUsage, config.budgets);
895
+ return {
896
+ taskId,
897
+ estimatedCostUsd,
898
+ passed: budget.passed,
899
+ warnings: budgetEstimateWarningsForBudgets(taskId, task.ownerRole, budget.usage, config.budgets),
900
+ budget,
901
+ };
902
+ }
667
903
  async function enforceLockPreflight(taskId, root) {
668
904
  const workspace = await loadWorkspace(root);
669
905
  const task = workspace.tasks.find((candidate) => candidate.id === taskId);
@@ -728,11 +964,71 @@ export async function listApprovals(taskId, root = process.cwd()) {
728
964
  : [];
729
965
  const events = await readEvents(root);
730
966
  const records = fileNames.map((fileName) => approvalRecordForArtifact(path.join(".agent-workflow", "approvals", fileName), events));
967
+ records.push(...gateApprovalRecords(events));
731
968
  const filtered = taskId
732
969
  ? records.filter((record) => record.taskId === taskId)
733
970
  : records;
734
971
  return filtered.sort((left, right) => left.id.localeCompare(right.id));
735
972
  }
973
+ export async function approveWorkflowGate(input, root = process.cwd()) {
974
+ const run = await readAutonomousRun(root, input.runId);
975
+ if (!run) {
976
+ throw new Error(`workflow run not found: ${input.runId}`);
977
+ }
978
+ const phaseIndex = run.phases.findLastIndex((phase) => phase.status === "gate_paused");
979
+ if (phaseIndex < 0) {
980
+ throw new Error(`workflow run ${input.runId} has no paused gate`);
981
+ }
982
+ const phase = run.phases[phaseIndex];
983
+ const gateId = phase.gateId ?? gateIdForPhase(run, phaseIndex);
984
+ if (input.gateId !== gateId) {
985
+ throw new Error(`wrong gate id: ${input.gateId}. Expected gate: ${gateId}`);
986
+ }
987
+ if (phase.approvedBy && phase.approvedAt) {
988
+ return {
989
+ run,
990
+ gateId,
991
+ approver: phase.approvedBy,
992
+ approvedAt: phase.approvedAt,
993
+ alreadyApproved: true,
994
+ };
995
+ }
996
+ const approvedAt = new Date().toISOString();
997
+ const approvedPhase = {
998
+ ...phase,
999
+ gateId,
1000
+ approvedBy: input.approver,
1001
+ approvedAt,
1002
+ approvalRationale: input.rationale,
1003
+ };
1004
+ const updatedRun = {
1005
+ ...run,
1006
+ phases: run.phases.map((candidate, index) => index === phaseIndex ? approvedPhase : candidate),
1007
+ updatedAt: approvedAt,
1008
+ };
1009
+ await persistRun(root, updatedRun);
1010
+ await appendEvent(root, removeUndefined({
1011
+ type: "GATE_APPROVED",
1012
+ taskId: run.taskId,
1013
+ actor: "parent",
1014
+ summary: `Gate approved: ${gateId}`,
1015
+ artifacts: phase.reviewArtifact ? [phase.reviewArtifact] : undefined,
1016
+ metadata: {
1017
+ runId: run.id,
1018
+ gateId,
1019
+ approver: input.approver,
1020
+ rationale: input.rationale,
1021
+ approvedAt,
1022
+ },
1023
+ }));
1024
+ return {
1025
+ run: updatedRun,
1026
+ gateId,
1027
+ approver: input.approver,
1028
+ approvedAt,
1029
+ alreadyApproved: false,
1030
+ };
1031
+ }
736
1032
  export async function showApproval(id, root = process.cwd()) {
737
1033
  const record = await getApprovalRecord(id, root);
738
1034
  const content = await readFile(path.join(root, record.artifact), "utf8");
@@ -744,7 +1040,7 @@ export async function approveApproval(input, root = process.cwd()) {
744
1040
  export async function rejectApproval(input, root = process.cwd()) {
745
1041
  return recordApprovalDecision(input, "rejected", root);
746
1042
  }
747
- export async function recordReview(input, root = process.cwd()) {
1043
+ export async function recordReview(input, root = process.cwd(), notificationTransports = {}) {
748
1044
  const workspace = await loadWorkspace(root);
749
1045
  const reviewInput = removeUndefined({
750
1046
  ...input,
@@ -770,6 +1066,17 @@ export async function recordReview(input, root = process.cwd()) {
770
1066
  artifacts: [artifact],
771
1067
  metadata: { result: reviewInput.result, severity: reviewInput.severity },
772
1068
  });
1069
+ if (reviewInput.result === "block" && reviewInput.severity === "critical") {
1070
+ await notifyWorkflowLifecycle({
1071
+ root,
1072
+ taskId: reviewInput.task,
1073
+ kind: "critical_blocking_review",
1074
+ actor: reviewInput.role,
1075
+ detail: reviewInput.findings,
1076
+ artifact,
1077
+ idempotencyKey: `critical_blocking_review:${reviewInput.task}:${artifact}`,
1078
+ }, notificationTransports);
1079
+ }
773
1080
  return { artifact, content };
774
1081
  }
775
1082
  function riskReviewSteps(impactAreas) {
@@ -982,6 +1289,7 @@ export async function completeWithProviderFallback(routing, prompt, { failingPro
982
1289
  const response = await provider.complete({
983
1290
  model: routing.model,
984
1291
  jsonMode,
1292
+ timeoutMs: routing.timeoutMs,
985
1293
  messages: [{ role: "user", content: prompt }],
986
1294
  });
987
1295
  if (index > 0) {
@@ -1002,6 +1310,9 @@ export async function completeWithProviderFallback(routing, prompt, { failingPro
1002
1310
  };
1003
1311
  }
1004
1312
  catch (error) {
1313
+ if (isProviderTimeoutError(error)) {
1314
+ throw error;
1315
+ }
1005
1316
  failedProviders.push({
1006
1317
  provider: providerId,
1007
1318
  reason: error instanceof Error ? error.message : String(error),
@@ -1012,6 +1323,14 @@ export async function completeWithProviderFallback(routing, prompt, { failingPro
1012
1323
  .map((failure) => failure.provider)
1013
1324
  .join(", ")}`);
1014
1325
  }
1326
+ function isProviderTimeoutError(error) {
1327
+ if (!(error instanceof Error)) {
1328
+ return String(error).toLowerCase().includes("timeout");
1329
+ }
1330
+ return (error.name === "TimeoutError" ||
1331
+ error.name === "AbortError" ||
1332
+ error.message.toLowerCase().includes("timeout"));
1333
+ }
1015
1334
  export async function setRoleModelProvider(role, routing, root = process.cwd()) {
1016
1335
  const workspace = await loadWorkspace(root);
1017
1336
  if (!workspace.roleIds.has(role)) {
@@ -1076,7 +1395,13 @@ export async function checkUsageBudget(taskId, root = process.cwd()) {
1076
1395
  }
1077
1396
  }
1078
1397
  const usage = await getUsageReport(taskId, root);
1079
- const budgets = config.budgets;
1398
+ const workspace = taskId ? await loadWorkspace(root) : undefined;
1399
+ const ownerRole = taskId
1400
+ ? workspace?.tasks.find((task) => task.id === taskId)?.ownerRole
1401
+ : undefined;
1402
+ return checkUsageBudgetForUsage(taskId, ownerRole, usage, config.budgets);
1403
+ }
1404
+ async function checkUsageBudgetForUsage(taskId, ownerRole, usage, budgets) {
1080
1405
  const appliedBudgets = [];
1081
1406
  const violations = [];
1082
1407
  if (budgets?.defaults) {
@@ -1094,6 +1419,12 @@ export async function checkUsageBudget(taskId, root = process.cwd()) {
1094
1419
  violations.push(...budgetViolations(`role:${roleUsage.key}`, roleUsage, roleBudget));
1095
1420
  }
1096
1421
  }
1422
+ if (ownerRole &&
1423
+ budgets?.byRole?.[ownerRole] &&
1424
+ !usage.byRole.some((roleUsage) => roleUsage.key === ownerRole)) {
1425
+ appliedBudgets.push(`role:${ownerRole}`);
1426
+ violations.push(...budgetViolations(`role:${ownerRole}`, emptyUsageBreakdown(ownerRole), budgets.byRole[ownerRole]));
1427
+ }
1097
1428
  return {
1098
1429
  ...(taskId ? { taskId } : {}),
1099
1430
  passed: violations.length === 0,
@@ -1102,6 +1433,39 @@ export async function checkUsageBudget(taskId, root = process.cwd()) {
1102
1433
  violations,
1103
1434
  };
1104
1435
  }
1436
+ async function enforceBudgetEstimatePreflight(taskId, root, estimatedCostUsd, override) {
1437
+ const preflight = await checkBudgetEstimatePreflight(taskId, estimatedCostUsd, root);
1438
+ if (preflight.warnings.length > 0) {
1439
+ await appendEvent(root, {
1440
+ type: "BUDGET_PREFLIGHT_WARNING",
1441
+ taskId,
1442
+ actor: "parent",
1443
+ summary: "Estimated cost reached budget warning threshold",
1444
+ metadata: { preflight },
1445
+ });
1446
+ }
1447
+ if (preflight.passed) {
1448
+ return;
1449
+ }
1450
+ if (override) {
1451
+ await appendEvent(root, {
1452
+ type: "BUDGET_PREFLIGHT_OVERRIDDEN",
1453
+ taskId,
1454
+ actor: "parent",
1455
+ summary: "Estimated cost budget block overridden",
1456
+ metadata: { preflight },
1457
+ });
1458
+ return;
1459
+ }
1460
+ await appendEvent(root, {
1461
+ type: "BUDGET_PREFLIGHT_BLOCKED",
1462
+ taskId,
1463
+ actor: "parent",
1464
+ summary: "Estimated cost exceeds configured budget",
1465
+ metadata: { preflight },
1466
+ });
1467
+ throw new Error("budget estimate exceeds configured limit; rerun with --yes to override");
1468
+ }
1105
1469
  async function createBudgetFallbackProposal(taskId, budget, root) {
1106
1470
  const workspace = await loadWorkspace(root);
1107
1471
  const task = workspace.tasks.find((candidate) => candidate.id === taskId);
@@ -1186,6 +1550,39 @@ function approvalRecordForArtifact(artifact, events) {
1186
1550
  : undefined,
1187
1551
  });
1188
1552
  }
1553
+ function gateApprovalRecords(events) {
1554
+ return events
1555
+ .filter((event) => event.type === "GATE_APPROVED")
1556
+ .map((event) => {
1557
+ const runId = typeof event.metadata.runId === "string" ? event.metadata.runId : "run";
1558
+ const gateId = typeof event.metadata.gateId === "string"
1559
+ ? event.metadata.gateId
1560
+ : "gate";
1561
+ return removeUndefined({
1562
+ id: `gate-${runId}-${gateId}`.replace(/[^a-zA-Z0-9._-]/g, "-"),
1563
+ taskId: event.taskId ?? undefined,
1564
+ artifact: event.artifacts?.[0] ?? "events.jsonl",
1565
+ status: "approved",
1566
+ summary: event.summary,
1567
+ requestedAt: event.timestamp,
1568
+ decidedAt: typeof event.metadata.approvedAt === "string"
1569
+ ? event.metadata.approvedAt
1570
+ : event.timestamp,
1571
+ approver: typeof event.metadata.approver === "string"
1572
+ ? event.metadata.approver
1573
+ : undefined,
1574
+ rationale: typeof event.metadata.rationale === "string"
1575
+ ? event.metadata.rationale
1576
+ : undefined,
1577
+ });
1578
+ });
1579
+ }
1580
+ function gateIdForPhase(run, phaseIndex) {
1581
+ const phase = run.phases[phaseIndex]?.phase;
1582
+ const sequenceIndex = AUTONOMOUS_PHASE_SEQUENCE.findIndex((candidate) => candidate.phase === phase);
1583
+ const next = AUTONOMOUS_PHASE_SEQUENCE[sequenceIndex + 1]?.phase ?? "end";
1584
+ return `${phase}->${next}`;
1585
+ }
1189
1586
  function approvalEventsForArtifact(events, artifact) {
1190
1587
  return events.filter((event) => event.artifacts?.includes(artifact));
1191
1588
  }
@@ -1258,6 +1655,60 @@ function budgetViolation(scope, metric, actual, limit) {
1258
1655
  limit,
1259
1656
  };
1260
1657
  }
1658
+ function projectUsageCost(usage, ownerRole, estimatedCostUsd) {
1659
+ const byRole = [...usage.byRole];
1660
+ const roleIndex = byRole.findIndex((entry) => entry.key === ownerRole);
1661
+ if (roleIndex >= 0) {
1662
+ const roleUsage = byRole[roleIndex] ?? emptyUsageBreakdown(ownerRole);
1663
+ byRole[roleIndex] = addEstimatedCost(roleUsage, estimatedCostUsd);
1664
+ }
1665
+ else {
1666
+ byRole.push(addEstimatedCost(emptyUsageBreakdown(ownerRole), estimatedCostUsd));
1667
+ }
1668
+ return {
1669
+ ...usage,
1670
+ totals: addEstimatedCost(usage.totals, estimatedCostUsd),
1671
+ byRole,
1672
+ };
1673
+ }
1674
+ function addEstimatedCost(usage, estimatedCostUsd) {
1675
+ return {
1676
+ ...usage,
1677
+ estimatedCostUsd: usage.estimatedCostUsd + estimatedCostUsd,
1678
+ };
1679
+ }
1680
+ function emptyUsageBreakdown(key) {
1681
+ return {
1682
+ key,
1683
+ requests: 0,
1684
+ inputTokens: 0,
1685
+ outputTokens: 0,
1686
+ totalTokens: 0,
1687
+ estimatedCostUsd: 0,
1688
+ };
1689
+ }
1690
+ function budgetEstimateWarningsForBudgets(taskId, ownerRole, usage, budgets) {
1691
+ const warnings = [];
1692
+ pushCostWarning(warnings, "defaults", usage.totals, budgets?.defaults);
1693
+ pushCostWarning(warnings, `task:${taskId}`, usage.totals, budgets?.byTask?.[taskId]);
1694
+ pushCostWarning(warnings, `role:${ownerRole}`, usage.byRole.find((roleUsage) => roleUsage.key === ownerRole) ??
1695
+ emptyUsageBreakdown(ownerRole), budgets?.byRole?.[ownerRole]);
1696
+ return warnings;
1697
+ }
1698
+ function pushCostWarning(warnings, scope, usage, budget) {
1699
+ const thresholdPercent = 80;
1700
+ const limit = budget?.maxEstimatedCostUsd;
1701
+ if (limit !== undefined &&
1702
+ usage.estimatedCostUsd >= limit * (thresholdPercent / 100)) {
1703
+ warnings.push({
1704
+ scope,
1705
+ metric: "estimatedCostUsd",
1706
+ actual: usage.estimatedCostUsd,
1707
+ limit,
1708
+ thresholdPercent,
1709
+ });
1710
+ }
1711
+ }
1261
1712
  async function writeBudgetEscalationProposal(root, taskId, proposal) {
1262
1713
  const content = [
1263
1714
  `# Budget Escalation Proposal: ${taskId}`,
@@ -1305,6 +1756,21 @@ async function mutateTasks(base, update) {
1305
1756
  async function mutateLocks(base, update) {
1306
1757
  return updateJsonFile(path.join(base, FILES.locks), [], update);
1307
1758
  }
1759
+ function assertTaskCanBeRemoved(task, tasks, isForced) {
1760
+ if (!isForced &&
1761
+ (task.status === "in_progress" || task.status === "blocked")) {
1762
+ throw new Error(`task ${task.id} is ${task.status}; use --force to remove it`);
1763
+ }
1764
+ const dependent = tasks.find((candidate) => candidate.id !== task.id &&
1765
+ !isTerminalTaskStatus(candidate.status) &&
1766
+ candidate.dependencies.includes(task.id));
1767
+ if (dependent) {
1768
+ throw new Error(`task ${task.id} is a dependency of active task ${dependent.id}`);
1769
+ }
1770
+ }
1771
+ function isTerminalTaskStatus(status) {
1772
+ return ["done", "canceled", "rejected", "archived"].includes(status);
1773
+ }
1308
1774
  function flowRequirementLines(requirements) {
1309
1775
  return requirements.length > 0
1310
1776
  ? requirements.map((requirement) => `- ${requirement}`)