@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.
- package/dist/assets/web-console.js +91 -1
- package/dist/autonomous-phase-lifecycle.d.ts +3 -2
- package/dist/autonomous-phase-lifecycle.js +28 -8
- package/dist/autonomous-phase-lifecycle.js.map +1 -1
- package/dist/autonomous-run-state.d.ts +6 -4
- package/dist/autonomous-run-state.js +82 -11
- package/dist/autonomous-run-state.js.map +1 -1
- package/dist/autonomous-run-store.d.ts +5 -0
- package/dist/autonomous-run-store.js +27 -2
- package/dist/autonomous-run-store.js.map +1 -1
- package/dist/autonomous-workflow-constants.d.ts +1 -0
- package/dist/autonomous-workflow-constants.js.map +1 -1
- package/dist/autonomous-workflow.d.ts +2 -2
- package/dist/autonomous-workflow.js +2 -2
- package/dist/autonomous-workflow.js.map +1 -1
- package/dist/benchmark.js +16 -0
- package/dist/benchmark.js.map +1 -1
- package/dist/clarification.d.ts +2 -0
- package/dist/clarification.js +23 -5
- package/dist/clarification.js.map +1 -1
- package/dist/cli.js +89 -14
- package/dist/cli.js.map +1 -1
- package/dist/command-manifest.js +8 -0
- package/dist/command-manifest.js.map +1 -1
- package/dist/commands.d.ts +9 -2
- package/dist/commands.js +456 -47
- package/dist/commands.js.map +1 -1
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -1
- package/dist/mcp-oauth-proxy.d.ts +47 -0
- package/dist/mcp-oauth-proxy.js +276 -0
- package/dist/mcp-oauth-proxy.js.map +1 -1
- package/dist/memory.d.ts +11 -0
- package/dist/memory.js +224 -0
- package/dist/memory.js.map +1 -0
- package/dist/metrics-commands.js +9 -0
- package/dist/metrics-commands.js.map +1 -1
- package/dist/model-commands.js +18 -1
- package/dist/model-commands.js.map +1 -1
- package/dist/notifications.d.ts +25 -0
- package/dist/notifications.js +187 -11
- package/dist/notifications.js.map +1 -1
- package/dist/package-update-check.d.ts +19 -0
- package/dist/package-update-check.js +123 -0
- package/dist/package-update-check.js.map +1 -0
- package/dist/runtime-bootstrap.js +4 -2
- package/dist/runtime-bootstrap.js.map +1 -1
- package/dist/runtime-execution-renderer.js +3 -0
- package/dist/runtime-execution-renderer.js.map +1 -1
- package/dist/runtime-execution.js +6 -0
- package/dist/runtime-execution.js.map +1 -1
- package/dist/skills-commands.d.ts +1 -0
- package/dist/skills-commands.js +9 -1
- package/dist/skills-commands.js.map +1 -1
- package/dist/skills.d.ts +7 -1
- package/dist/skills.js +120 -11
- package/dist/skills.js.map +1 -1
- package/dist/subagent-protocol.js +6 -0
- package/dist/subagent-protocol.js.map +1 -1
- package/dist/tool-commands.d.ts +2 -0
- package/dist/tool-commands.js +105 -13
- package/dist/tool-commands.js.map +1 -1
- package/dist/types.d.ts +127 -4
- package/dist/types.js.map +1 -1
- package/dist/web-api.js +93 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-chart-contracts.d.ts +3 -1
- package/dist/web-chart-contracts.js +6 -0
- package/dist/web-chart-contracts.js.map +1 -1
- package/dist/web-console.js +2 -2
- package/dist/workflow-services.d.ts +21 -3
- package/dist/workflow-services.js +474 -10
- package/dist/workflow-services.js.map +1 -1
- package/package.json +2 -1
- package/skills/proactive-orchestra/SKILL.md +27 -0
- 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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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}`)
|