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