@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.
- package/AGENTS.md +5 -3
- package/dist/advisory-artifacts.d.ts +14 -0
- package/dist/advisory-artifacts.js +100 -0
- package/dist/advisory-artifacts.js.map +1 -0
- package/dist/assets/web-console.js +230 -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 +97 -18
- package/dist/cli.js.map +1 -1
- package/dist/command-manifest.d.ts +3 -0
- package/dist/command-manifest.js +17 -1
- package/dist/command-manifest.js.map +1 -1
- package/dist/commands.d.ts +10 -2
- package/dist/commands.js +484 -49
- package/dist/commands.js.map +1 -1
- package/dist/constants.js +13 -0
- package/dist/constants.js.map +1 -1
- package/dist/context-budget.d.ts +4 -0
- package/dist/context-budget.js +119 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/mcp-oauth-proxy.d.ts +79 -0
- package/dist/mcp-oauth-proxy.js +396 -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-commands.js +10 -0
- package/dist/runtime-commands.js.map +1 -1
- package/dist/runtime-execution-renderer.js +5 -0
- package/dist/runtime-execution-renderer.js.map +1 -1
- package/dist/runtime-execution.d.ts +4 -2
- package/dist/runtime-execution.js +17 -4
- package/dist/runtime-execution.js.map +1 -1
- package/dist/setup-agents-import.d.ts +42 -0
- package/dist/setup-agents-import.js +337 -0
- package/dist/setup-agents-import.js.map +1 -0
- 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 +113 -13
- package/dist/tool-commands.js.map +1 -1
- package/dist/types.d.ts +163 -4
- package/dist/types.js.map +1 -1
- package/dist/web-api.js +216 -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 +24 -4
- package/dist/workflow-services.js +478 -12
- package/dist/workflow-services.js.map +1 -1
- package/dist/workflow-templates.d.ts +1 -0
- package/dist/workflow-templates.js +1 -0
- package/dist/workflow-templates.js.map +1 -1
- package/dist/workspace.js +6 -0
- package/dist/workspace.js.map +1 -1
- package/docs/command-contracts.md +22 -0
- package/docs/mcp-oauth-proxy-evaluation.md +14 -0
- package/docs/runtime-adapters.md +4 -0
- package/docs/setup-agents-bridge.md +61 -0
- package/docs/traceability-flow.md +89 -0
- package/package.json +8 -6
- package/skills/proactive-orchestra/SKILL.md +27 -0
- 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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
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}`)
|