@jterrats/open-orchestra 0.1.0
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 +151 -0
- package/CLAUDE.md +157 -0
- package/README.md +60 -0
- package/bin/orchestra.js +8 -0
- package/dist/args.d.ts +3 -0
- package/dist/args.js +30 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +190 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +44 -0
- package/dist/commands.js +883 -0
- package/dist/commands.js.map +1 -0
- package/dist/constants.d.ts +15 -0
- package/dist/constants.js +69 -0
- package/dist/constants.js.map +1 -0
- package/dist/defaults.d.ts +72 -0
- package/dist/defaults.js +694 -0
- package/dist/defaults.js.map +1 -0
- package/dist/fs-utils.d.ts +8 -0
- package/dist/fs-utils.js +35 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/model-providers.d.ts +19 -0
- package/dist/model-providers.js +78 -0
- package/dist/model-providers.js.map +1 -0
- package/dist/types.d.ts +550 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +10 -0
- package/dist/validation.js +163 -0
- package/dist/validation.js.map +1 -0
- package/dist/web-api.d.ts +16 -0
- package/dist/web-api.js +220 -0
- package/dist/web-api.js.map +1 -0
- package/dist/web-chart-contracts.d.ts +13 -0
- package/dist/web-chart-contracts.js +13 -0
- package/dist/web-chart-contracts.js.map +1 -0
- package/dist/web-console.d.ts +1 -0
- package/dist/web-console.js +232 -0
- package/dist/web-console.js.map +1 -0
- package/dist/web-evidence.d.ts +25 -0
- package/dist/web-evidence.js +67 -0
- package/dist/web-evidence.js.map +1 -0
- package/dist/web-playwright.d.ts +3 -0
- package/dist/web-playwright.js +14 -0
- package/dist/web-playwright.js.map +1 -0
- package/dist/web-roles.d.ts +33 -0
- package/dist/web-roles.js +70 -0
- package/dist/web-roles.js.map +1 -0
- package/dist/workflow-gates.d.ts +7 -0
- package/dist/workflow-gates.js +291 -0
- package/dist/workflow-gates.js.map +1 -0
- package/dist/workflow-services.d.ts +56 -0
- package/dist/workflow-services.js +1240 -0
- package/dist/workflow-services.js.map +1 -0
- package/dist/workspace-validator.d.ts +6 -0
- package/dist/workspace-validator.js +189 -0
- package/dist/workspace-validator.js.map +1 -0
- package/dist/workspace.d.ts +10 -0
- package/dist/workspace.js +72 -0
- package/dist/workspace.js.map +1 -0
- package/docs/multi-agent-orchestrator-backlog.md +445 -0
- package/docs/multi-agent-orchestrator-sprint-1.md +433 -0
- package/docs/orchestra-mvp.md +176 -0
- package/package.json +63 -0
- package/rules/agent-collaboration.mdc +58 -0
- package/rules/agent-roles.mdc +105 -0
- package/rules/ai-assisted-development.mdc +31 -0
- package/rules/api-design.mdc +31 -0
- package/rules/architecture-decisions.mdc +27 -0
- package/rules/code-review-engineering.mdc +34 -0
- package/rules/concurrency-async.mdc +32 -0
- package/rules/configuration-management.mdc +31 -0
- package/rules/data-modeling-domain.mdc +31 -0
- package/rules/delivery-quality-gates.mdc +40 -0
- package/rules/dependency-management.mdc +31 -0
- package/rules/devops-tooling.mdc +55 -0
- package/rules/documentation-standards.mdc +26 -0
- package/rules/dry-clean-code.mdc +30 -0
- package/rules/error-handling.mdc +28 -0
- package/rules/frontend-engineering.mdc +32 -0
- package/rules/git-discipline.mdc +39 -0
- package/rules/infra-data-encryption.mdc +81 -0
- package/rules/performance-reliability.mdc +32 -0
- package/rules/readiness-done.mdc +32 -0
- package/rules/release-rollback.mdc +32 -0
- package/rules/rule-composition.mdc +28 -0
- package/rules/security-guardrails.mdc +37 -0
- package/rules/solid-architecture.mdc +32 -0
- package/rules/static-analysis-githooks.mdc +32 -0
- package/rules/testing-discipline.mdc +42 -0
- package/rules/ux-ui-product-experience.mdc +51 -0
- package/rules/work-intake-sequencing.mdc +39 -0
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { FILES } from "./constants.js";
|
|
4
|
+
import { exists, ensureDir, readJson, resolveWorkflowPath, writeJson, } from "./fs-utils.js";
|
|
5
|
+
import { FakeModelProvider, InMemoryModelProviderRegistry, summarizeConfiguredProviders, } from "./model-providers.js";
|
|
6
|
+
import { appendEvent, loadWorkspace, readEvents, writeArtifact, } from "./workspace.js";
|
|
7
|
+
import { validateEvidenceInput, validateReadiness, validateReviewInput, } from "./validation.js";
|
|
8
|
+
import { getWorkflowGate } from "./workflow-gates.js";
|
|
9
|
+
export async function getWorkflowStatus(root = process.cwd()) {
|
|
10
|
+
const workspace = await loadWorkspace(root);
|
|
11
|
+
const counts = Object.create(null);
|
|
12
|
+
const blocked = [];
|
|
13
|
+
for (const task of workspace.tasks) {
|
|
14
|
+
counts[task.status] = (counts[task.status] ?? 0) + 1;
|
|
15
|
+
if (task.status === "blocked") {
|
|
16
|
+
blocked.push({
|
|
17
|
+
id: task.id,
|
|
18
|
+
title: task.title,
|
|
19
|
+
reason: task.blockedReason ?? "not specified",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
tasks: {
|
|
25
|
+
total: workspace.tasks.length,
|
|
26
|
+
byStatus: counts,
|
|
27
|
+
blocked,
|
|
28
|
+
},
|
|
29
|
+
locks: {
|
|
30
|
+
total: workspace.locks.length,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function addTask(input, root = process.cwd()) {
|
|
35
|
+
const workspace = await loadWorkspace(root);
|
|
36
|
+
if (!workspace.roleIds.has(input.ownerRole)) {
|
|
37
|
+
throw new Error(`unknown owner role: ${input.ownerRole}`);
|
|
38
|
+
}
|
|
39
|
+
if (workspace.tasks.some((task) => task.id === input.id)) {
|
|
40
|
+
throw new Error(`task already exists: ${input.id}`);
|
|
41
|
+
}
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const task = removeUndefined({
|
|
44
|
+
...input,
|
|
45
|
+
status: "pending",
|
|
46
|
+
createdAt: now,
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
});
|
|
49
|
+
await writeTasks(workspace.base, [...workspace.tasks, task]);
|
|
50
|
+
await appendEvent(root, {
|
|
51
|
+
type: "TASK_ASSIGNED",
|
|
52
|
+
taskId: input.id,
|
|
53
|
+
actor: "parent",
|
|
54
|
+
summary: `Task assigned to ${input.ownerRole}`,
|
|
55
|
+
metadata: { title: input.title, ownerRole: input.ownerRole },
|
|
56
|
+
});
|
|
57
|
+
return task;
|
|
58
|
+
}
|
|
59
|
+
export async function listTasks(root = process.cwd()) {
|
|
60
|
+
const workspace = await loadWorkspace(root);
|
|
61
|
+
return workspace.tasks;
|
|
62
|
+
}
|
|
63
|
+
export async function listRoles(root = process.cwd()) {
|
|
64
|
+
const workspace = await loadWorkspace(root);
|
|
65
|
+
return workspace.roles;
|
|
66
|
+
}
|
|
67
|
+
export async function updateTask(input, root = process.cwd()) {
|
|
68
|
+
const workspace = await loadWorkspace(root);
|
|
69
|
+
const taskIndex = workspace.tasks.findIndex((task) => task.id === input.id);
|
|
70
|
+
if (taskIndex < 0) {
|
|
71
|
+
throw new Error(`unknown task: ${input.id}`);
|
|
72
|
+
}
|
|
73
|
+
const current = workspace.tasks[taskIndex];
|
|
74
|
+
if (!current) {
|
|
75
|
+
throw new Error(`unknown task: ${input.id}`);
|
|
76
|
+
}
|
|
77
|
+
const updated = removeUndefined({
|
|
78
|
+
...current,
|
|
79
|
+
status: input.status,
|
|
80
|
+
blockedReason: input.blockedReason,
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
const tasks = [...workspace.tasks];
|
|
84
|
+
tasks[taskIndex] = updated;
|
|
85
|
+
await writeTasks(workspace.base, tasks);
|
|
86
|
+
await appendEvent(root, {
|
|
87
|
+
type: "TASK_UPDATED",
|
|
88
|
+
taskId: input.id,
|
|
89
|
+
actor: "parent",
|
|
90
|
+
summary: `Task updated: ${input.id}`,
|
|
91
|
+
metadata: { status: updated.status },
|
|
92
|
+
});
|
|
93
|
+
return updated;
|
|
94
|
+
}
|
|
95
|
+
export async function checkTaskDependencies(taskId, root = process.cwd()) {
|
|
96
|
+
const workspace = await loadWorkspace(root);
|
|
97
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
98
|
+
if (!task) {
|
|
99
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
100
|
+
}
|
|
101
|
+
const dependencies = task.dependencies.map((dependencyId) => {
|
|
102
|
+
const dependency = workspace.tasks.find((candidate) => candidate.id === dependencyId);
|
|
103
|
+
if (!dependency) {
|
|
104
|
+
throw new Error(`task ${taskId} has unknown dependency: ${dependencyId}`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: dependency.id,
|
|
108
|
+
status: dependency.status,
|
|
109
|
+
isComplete: ["approved", "done"].includes(dependency.status),
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
const incomplete = dependencies.filter((dependency) => !dependency.isComplete);
|
|
113
|
+
return {
|
|
114
|
+
taskId,
|
|
115
|
+
isSatisfied: incomplete.length === 0,
|
|
116
|
+
dependencies,
|
|
117
|
+
incomplete,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export async function generateTaskGraphPlan(root = process.cwd()) {
|
|
121
|
+
const tasks = await listTasks(root);
|
|
122
|
+
const locks = await listLocks(root);
|
|
123
|
+
const ready = [];
|
|
124
|
+
const blocked = [];
|
|
125
|
+
const locked = [];
|
|
126
|
+
const complete = [];
|
|
127
|
+
for (const task of tasks) {
|
|
128
|
+
const item = {
|
|
129
|
+
id: task.id,
|
|
130
|
+
title: task.title,
|
|
131
|
+
status: task.status,
|
|
132
|
+
ownerRole: task.ownerRole,
|
|
133
|
+
};
|
|
134
|
+
if (["approved", "done"].includes(task.status)) {
|
|
135
|
+
complete.push(item);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (task.status === "canceled") {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const dependencies = await checkTaskDependencies(task.id, root);
|
|
142
|
+
if (dependencies.isSatisfied) {
|
|
143
|
+
const taskLocks = locksForTask(task, locks);
|
|
144
|
+
if (taskLocks.length > 0) {
|
|
145
|
+
locked.push({
|
|
146
|
+
...item,
|
|
147
|
+
locks: taskLocks,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
ready.push(item);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
blocked.push({
|
|
156
|
+
...item,
|
|
157
|
+
incomplete: dependencies.incomplete,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { ready, blocked, locked, complete };
|
|
162
|
+
}
|
|
163
|
+
export async function executeNextReadyTask(root = process.cwd()) {
|
|
164
|
+
const plan = await generateTaskGraphPlan(root);
|
|
165
|
+
const next = plan.ready[0];
|
|
166
|
+
if (!next) {
|
|
167
|
+
throw new Error("no ready tasks to run");
|
|
168
|
+
}
|
|
169
|
+
return executePlanWithBudgetPreflight(next.id, root);
|
|
170
|
+
}
|
|
171
|
+
export async function executeReadyTaskBatch(root = process.cwd()) {
|
|
172
|
+
const plan = await generateTaskGraphPlan(root);
|
|
173
|
+
const selectedTaskIds = plan.ready.map((task) => task.id);
|
|
174
|
+
if (selectedTaskIds.length === 0) {
|
|
175
|
+
throw new Error("no ready tasks to run");
|
|
176
|
+
}
|
|
177
|
+
const runs = [];
|
|
178
|
+
for (const taskId of selectedTaskIds) {
|
|
179
|
+
try {
|
|
180
|
+
runs.push(await executePlanWithBudgetPreflight(taskId, root));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const batch = {
|
|
184
|
+
selectedTaskIds,
|
|
185
|
+
runs,
|
|
186
|
+
locked: plan.locked,
|
|
187
|
+
failure: {
|
|
188
|
+
taskId,
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
return recordTaskGraphBatchRun(batch, root);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return recordTaskGraphBatchRun({ selectedTaskIds, runs, locked: plan.locked }, root);
|
|
196
|
+
}
|
|
197
|
+
export async function listLocks(root = process.cwd()) {
|
|
198
|
+
const workspace = await loadWorkspace(root);
|
|
199
|
+
return workspace.locks;
|
|
200
|
+
}
|
|
201
|
+
export async function claimLock(input, root = process.cwd()) {
|
|
202
|
+
const workspace = await loadWorkspace(root);
|
|
203
|
+
if (!workspace.tasks.some((task) => task.id === input.taskId)) {
|
|
204
|
+
throw new Error(`unknown task: ${input.taskId}`);
|
|
205
|
+
}
|
|
206
|
+
if (!workspace.roleIds.has(input.ownerRole)) {
|
|
207
|
+
throw new Error(`unknown owner role: ${input.ownerRole}`);
|
|
208
|
+
}
|
|
209
|
+
const conflictingLock = workspace.locks.find((lock) => lock.path === input.path);
|
|
210
|
+
if (conflictingLock) {
|
|
211
|
+
throw new Error(`path already locked by ${conflictingLock.id}`);
|
|
212
|
+
}
|
|
213
|
+
const now = new Date().toISOString();
|
|
214
|
+
const lock = removeUndefined({
|
|
215
|
+
id: input.id ?? `lock-${Date.now()}`,
|
|
216
|
+
taskId: input.taskId,
|
|
217
|
+
ownerRole: input.ownerRole,
|
|
218
|
+
path: input.path,
|
|
219
|
+
reason: input.reason,
|
|
220
|
+
createdAt: now,
|
|
221
|
+
expiresAt: input.expiresAt,
|
|
222
|
+
});
|
|
223
|
+
await writeLocks(workspace.base, [...workspace.locks, lock]);
|
|
224
|
+
await appendEvent(root, {
|
|
225
|
+
type: "LOCK_CLAIMED",
|
|
226
|
+
taskId: input.taskId,
|
|
227
|
+
actor: input.ownerRole,
|
|
228
|
+
summary: `Lock claimed for ${input.path}`,
|
|
229
|
+
metadata: { lockId: lock.id, path: input.path },
|
|
230
|
+
});
|
|
231
|
+
return lock;
|
|
232
|
+
}
|
|
233
|
+
export async function releaseLock(id, root = process.cwd()) {
|
|
234
|
+
const workspace = await loadWorkspace(root);
|
|
235
|
+
const lock = workspace.locks.find((candidate) => candidate.id === id);
|
|
236
|
+
if (!lock) {
|
|
237
|
+
throw new Error(`unknown lock: ${id}`);
|
|
238
|
+
}
|
|
239
|
+
await writeLocks(workspace.base, workspace.locks.filter((candidate) => candidate.id !== id));
|
|
240
|
+
await appendEvent(root, {
|
|
241
|
+
type: "LOCK_RELEASED",
|
|
242
|
+
taskId: lock.taskId,
|
|
243
|
+
actor: lock.ownerRole,
|
|
244
|
+
summary: `Lock released for ${lock.path}`,
|
|
245
|
+
metadata: { lockId: lock.id, path: lock.path },
|
|
246
|
+
});
|
|
247
|
+
return lock;
|
|
248
|
+
}
|
|
249
|
+
export async function checkReadiness(taskId, root = process.cwd()) {
|
|
250
|
+
const workspace = await loadWorkspace(root);
|
|
251
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
252
|
+
if (!task) {
|
|
253
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
254
|
+
}
|
|
255
|
+
const report = validateReadiness(task);
|
|
256
|
+
await appendEvent(root, {
|
|
257
|
+
type: report.isReady ? "GATE_PASSED" : "GATE_BLOCKED",
|
|
258
|
+
taskId,
|
|
259
|
+
actor: "parent",
|
|
260
|
+
summary: report.isReady
|
|
261
|
+
? "Definition of Ready passed"
|
|
262
|
+
: "Definition of Ready blocked",
|
|
263
|
+
metadata: { ...report },
|
|
264
|
+
});
|
|
265
|
+
return { task, report };
|
|
266
|
+
}
|
|
267
|
+
export async function evaluateWorkflowGate(gateId, taskId, root = process.cwd()) {
|
|
268
|
+
const workspace = await loadWorkspace(root);
|
|
269
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
270
|
+
if (!task) {
|
|
271
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
272
|
+
}
|
|
273
|
+
const gate = getWorkflowGate(gateId);
|
|
274
|
+
const result = gate.evaluate({
|
|
275
|
+
task,
|
|
276
|
+
events: await readEvents(root),
|
|
277
|
+
locks: workspace.locks,
|
|
278
|
+
});
|
|
279
|
+
await appendEvent(root, {
|
|
280
|
+
type: result.passed ? "GATE_PASSED" : "GATE_BLOCKED",
|
|
281
|
+
taskId,
|
|
282
|
+
actor: "parent",
|
|
283
|
+
summary: result.summary,
|
|
284
|
+
metadata: {
|
|
285
|
+
gateId: result.gateId,
|
|
286
|
+
blocking: result.blocking,
|
|
287
|
+
missing: result.missing,
|
|
288
|
+
...result.metadata,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
export async function createHandoff(input, root = process.cwd()) {
|
|
294
|
+
const workspace = await loadWorkspace(root);
|
|
295
|
+
if (!workspace.roleIds.has(input.from) || !workspace.roleIds.has(input.to)) {
|
|
296
|
+
throw new Error("handoff roles must exist in roles.json");
|
|
297
|
+
}
|
|
298
|
+
const fileName = `${input.task}-${input.from}-to-${input.to}.md`;
|
|
299
|
+
const content = [
|
|
300
|
+
`# Handoff ${input.task}: ${input.from} to ${input.to}`,
|
|
301
|
+
"",
|
|
302
|
+
`- Status: ${input.status ?? "ready_for_review"}`,
|
|
303
|
+
`- Changed components: ${input.changed}`,
|
|
304
|
+
`- Behavior changed: ${input.behavior}`,
|
|
305
|
+
`- Unit tests: ${input.tests}`,
|
|
306
|
+
`- Commands run: ${input.commands}`,
|
|
307
|
+
`- Known gaps: ${input.gaps ?? "none"}`,
|
|
308
|
+
`- Risks: ${input.risks ?? "none"}`,
|
|
309
|
+
`- Recommended Playwright coverage: ${input.playwright ?? "not applicable"}`,
|
|
310
|
+
"",
|
|
311
|
+
].join("\n");
|
|
312
|
+
const artifact = await writeArtifact(root, "handoffs", fileName, content);
|
|
313
|
+
await appendEvent(root, {
|
|
314
|
+
type: "HANDOFF_READY",
|
|
315
|
+
taskId: input.task,
|
|
316
|
+
actor: input.from,
|
|
317
|
+
summary: `Handoff ready for ${input.to}`,
|
|
318
|
+
artifacts: [artifact],
|
|
319
|
+
});
|
|
320
|
+
return { artifact, content };
|
|
321
|
+
}
|
|
322
|
+
export async function recordDecision(input, root = process.cwd()) {
|
|
323
|
+
const workspace = await loadWorkspace(root);
|
|
324
|
+
if (!workspace.tasks.some((task) => task.id === input.task)) {
|
|
325
|
+
throw new Error(`unknown task: ${input.task}`);
|
|
326
|
+
}
|
|
327
|
+
if (!workspace.roleIds.has(input.owner)) {
|
|
328
|
+
throw new Error(`unknown owner role: ${input.owner}`);
|
|
329
|
+
}
|
|
330
|
+
const fileName = `${input.task}-${Date.now()}-decision.md`;
|
|
331
|
+
const content = [
|
|
332
|
+
`# Decision ${input.task}: ${input.title}`,
|
|
333
|
+
"",
|
|
334
|
+
`- Status: ${input.status}`,
|
|
335
|
+
`- Owner: ${input.owner}`,
|
|
336
|
+
"",
|
|
337
|
+
"## Context",
|
|
338
|
+
input.context,
|
|
339
|
+
"",
|
|
340
|
+
"## Decision",
|
|
341
|
+
input.decision,
|
|
342
|
+
"",
|
|
343
|
+
"## Consequences",
|
|
344
|
+
input.consequences,
|
|
345
|
+
"",
|
|
346
|
+
].join("\n");
|
|
347
|
+
const artifact = await writeArtifact(root, "decisions", fileName, content);
|
|
348
|
+
await appendEvent(root, {
|
|
349
|
+
type: "DECISION_RECORDED",
|
|
350
|
+
taskId: input.task,
|
|
351
|
+
actor: input.owner,
|
|
352
|
+
summary: input.title,
|
|
353
|
+
artifacts: [artifact],
|
|
354
|
+
metadata: {
|
|
355
|
+
status: input.status,
|
|
356
|
+
title: input.title,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
return { ...input, artifact };
|
|
360
|
+
}
|
|
361
|
+
export async function listDecisions(taskId, root = process.cwd()) {
|
|
362
|
+
return filterEvents("DECISION_RECORDED", taskId, root);
|
|
363
|
+
}
|
|
364
|
+
export async function getTaskContext(taskId, root = process.cwd()) {
|
|
365
|
+
const workspace = await loadWorkspace(root);
|
|
366
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
367
|
+
if (!task) {
|
|
368
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
369
|
+
}
|
|
370
|
+
const events = await readEvents(root);
|
|
371
|
+
const taskEvents = events.filter((event) => event.taskId === taskId);
|
|
372
|
+
return {
|
|
373
|
+
task,
|
|
374
|
+
dependencies: await checkTaskDependencies(taskId, root),
|
|
375
|
+
locks: workspace.locks.filter((lock) => lock.taskId === taskId),
|
|
376
|
+
decisions: taskEvents.filter((event) => event.type === "DECISION_RECORDED"),
|
|
377
|
+
handoffs: taskEvents.filter((event) => event.type === "HANDOFF_READY"),
|
|
378
|
+
reviews: taskEvents.filter((event) => event.type === "REVIEW_RECORDED"),
|
|
379
|
+
evidence: taskEvents.filter((event) => event.type === "EVIDENCE_ADDED"),
|
|
380
|
+
gates: taskEvents.filter((event) => event.type === "GATE_PASSED" || event.type === "GATE_BLOCKED"),
|
|
381
|
+
modelProvenance: await listModelProvenance(taskId, root),
|
|
382
|
+
risks: task.risks ?? [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
export async function generateExecutionPlan(taskId, root = process.cwd()) {
|
|
386
|
+
const context = await getTaskContext(taskId, root);
|
|
387
|
+
const steps = [
|
|
388
|
+
{
|
|
389
|
+
id: "context",
|
|
390
|
+
role: "parent",
|
|
391
|
+
action: "Review shared task context, dependencies, decisions, risks, and evidence.",
|
|
392
|
+
dependsOn: [],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: "architecture",
|
|
396
|
+
role: "architect",
|
|
397
|
+
action: "Confirm proposed architecture and user approval for non-trivial work.",
|
|
398
|
+
dependsOn: ["context"],
|
|
399
|
+
gate: "architecture",
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "development",
|
|
403
|
+
role: context.task.ownerRole,
|
|
404
|
+
action: "Implement domain/model changes, services/integrations, entry points, and unit tests.",
|
|
405
|
+
dependsOn: ["architecture"],
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "qa",
|
|
409
|
+
role: "qa",
|
|
410
|
+
action: "Review developer handoff, execute QA plan, and attach evidence.",
|
|
411
|
+
dependsOn: ["development"],
|
|
412
|
+
gate: "qa-release",
|
|
413
|
+
},
|
|
414
|
+
...riskReviewSteps(context.task.riskGate?.impactAreas ?? []),
|
|
415
|
+
{
|
|
416
|
+
id: "release-readiness",
|
|
417
|
+
role: "release_manager",
|
|
418
|
+
action: "Evaluate release readiness and unresolved blockers.",
|
|
419
|
+
dependsOn: [
|
|
420
|
+
"qa",
|
|
421
|
+
...riskReviewSteps(context.task.riskGate?.impactAreas ?? []).map((step) => step.id),
|
|
422
|
+
],
|
|
423
|
+
gate: "release-readiness",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: "summary",
|
|
427
|
+
role: "parent",
|
|
428
|
+
action: "Generate PR summary with evidence, reviews, gates, rollout, and rollback.",
|
|
429
|
+
dependsOn: ["release-readiness"],
|
|
430
|
+
},
|
|
431
|
+
];
|
|
432
|
+
return { taskId, steps };
|
|
433
|
+
}
|
|
434
|
+
export async function executePlanWithFakeProvider(taskId, root = process.cwd(), { providerId = "fake", model = "fake-model", budgetEscalation, } = {}) {
|
|
435
|
+
const plan = await generateExecutionPlan(taskId, root);
|
|
436
|
+
const provider = new FakeModelProvider({ id: providerId });
|
|
437
|
+
const steps = [];
|
|
438
|
+
for (const step of plan.steps) {
|
|
439
|
+
await appendEvent(root, {
|
|
440
|
+
type: "ORCH_STEP_STARTED",
|
|
441
|
+
taskId,
|
|
442
|
+
actor: step.role,
|
|
443
|
+
summary: `Started ${step.id}`,
|
|
444
|
+
metadata: { stepId: step.id },
|
|
445
|
+
});
|
|
446
|
+
try {
|
|
447
|
+
const response = await provider.complete({
|
|
448
|
+
model,
|
|
449
|
+
messages: [
|
|
450
|
+
{
|
|
451
|
+
role: "user",
|
|
452
|
+
content: `${step.role}: ${step.action}`,
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
await recordModelProvenance({
|
|
457
|
+
task: taskId,
|
|
458
|
+
role: step.role,
|
|
459
|
+
provider: response.provider,
|
|
460
|
+
model: response.model,
|
|
461
|
+
promptId: `plan:${taskId}:${step.id}`,
|
|
462
|
+
responseId: response.id,
|
|
463
|
+
inputTokens: response.usage.inputTokens,
|
|
464
|
+
outputTokens: response.usage.outputTokens,
|
|
465
|
+
estimatedCostUsd: 0,
|
|
466
|
+
finishReason: response.finishReason,
|
|
467
|
+
}, root);
|
|
468
|
+
const artifact = await writeRunArtifact(root, taskId, step.id, [
|
|
469
|
+
`# Run ${taskId}: ${step.id}`,
|
|
470
|
+
"",
|
|
471
|
+
`- Role: ${step.role}`,
|
|
472
|
+
`- Provider: ${response.provider}`,
|
|
473
|
+
`- Model: ${response.model}`,
|
|
474
|
+
`- Response ID: ${response.id}`,
|
|
475
|
+
"",
|
|
476
|
+
"## Output",
|
|
477
|
+
response.content,
|
|
478
|
+
"",
|
|
479
|
+
].join("\n"));
|
|
480
|
+
await appendEvent(root, {
|
|
481
|
+
type: "ORCH_STEP_COMPLETED",
|
|
482
|
+
taskId,
|
|
483
|
+
actor: step.role,
|
|
484
|
+
summary: `Completed ${step.id}`,
|
|
485
|
+
artifacts: [artifact],
|
|
486
|
+
metadata: { stepId: step.id, responseId: response.id },
|
|
487
|
+
});
|
|
488
|
+
steps.push({
|
|
489
|
+
id: step.id,
|
|
490
|
+
role: step.role,
|
|
491
|
+
status: "completed",
|
|
492
|
+
provider: response.provider,
|
|
493
|
+
model: response.model,
|
|
494
|
+
responseId: response.id,
|
|
495
|
+
artifact,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
500
|
+
await appendEvent(root, {
|
|
501
|
+
type: "ORCH_STEP_FAILED",
|
|
502
|
+
taskId,
|
|
503
|
+
actor: step.role,
|
|
504
|
+
summary: `Failed ${step.id}`,
|
|
505
|
+
metadata: { stepId: step.id, error: message },
|
|
506
|
+
});
|
|
507
|
+
steps.push({
|
|
508
|
+
id: step.id,
|
|
509
|
+
role: step.role,
|
|
510
|
+
status: "failed",
|
|
511
|
+
provider: provider.id,
|
|
512
|
+
model,
|
|
513
|
+
error: message,
|
|
514
|
+
});
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return removeUndefined({
|
|
519
|
+
taskId,
|
|
520
|
+
provider: provider.id,
|
|
521
|
+
model,
|
|
522
|
+
steps,
|
|
523
|
+
budgetEscalation,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
export async function executePlanWithBudgetPreflight(taskId, root = process.cwd(), decision) {
|
|
527
|
+
await enforceLockPreflight(taskId, root);
|
|
528
|
+
await enforceDependencyPreflight(taskId, root);
|
|
529
|
+
const budget = await checkUsageBudget(taskId, root);
|
|
530
|
+
if (budget.passed) {
|
|
531
|
+
return executePlanWithFakeProvider(taskId, root);
|
|
532
|
+
}
|
|
533
|
+
const proposal = await createBudgetFallbackProposal(taskId, budget, root);
|
|
534
|
+
const artifacts = proposal?.artifact ? [proposal.artifact] : undefined;
|
|
535
|
+
const storedApproval = proposal
|
|
536
|
+
? await findStoredApprovalForProposal(proposal, root)
|
|
537
|
+
: undefined;
|
|
538
|
+
await appendEvent(root, removeUndefined({
|
|
539
|
+
type: "BUDGET_ESCALATION_REQUESTED",
|
|
540
|
+
taskId,
|
|
541
|
+
actor: "parent",
|
|
542
|
+
summary: "Budget fallback approval requested",
|
|
543
|
+
artifacts,
|
|
544
|
+
metadata: { proposal },
|
|
545
|
+
}));
|
|
546
|
+
if (!decision && proposal && storedApproval?.status === "approved") {
|
|
547
|
+
return executeApprovedBudgetFallback(taskId, root, proposal, removeUndefined({
|
|
548
|
+
approver: storedApproval.approver,
|
|
549
|
+
rationale: storedApproval.rationale,
|
|
550
|
+
approvalId: storedApproval.id,
|
|
551
|
+
approvalSource: "stored",
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
if (!proposal || !decision?.approved) {
|
|
555
|
+
await appendEvent(root, removeUndefined({
|
|
556
|
+
type: "BUDGET_ESCALATION_REJECTED",
|
|
557
|
+
taskId,
|
|
558
|
+
actor: "parent",
|
|
559
|
+
summary: "Budget fallback approval rejected",
|
|
560
|
+
artifacts,
|
|
561
|
+
metadata: {
|
|
562
|
+
proposal,
|
|
563
|
+
approver: decision?.approver,
|
|
564
|
+
rationale: decision?.rationale ?? "approval not provided",
|
|
565
|
+
},
|
|
566
|
+
}));
|
|
567
|
+
throw new Error("budget fallback approval required");
|
|
568
|
+
}
|
|
569
|
+
await appendEvent(root, removeUndefined({
|
|
570
|
+
type: "BUDGET_ESCALATION_APPROVED",
|
|
571
|
+
taskId,
|
|
572
|
+
actor: "parent",
|
|
573
|
+
summary: `Budget fallback approved: ${proposal.toProvider}/${proposal.toModel}`,
|
|
574
|
+
artifacts,
|
|
575
|
+
metadata: {
|
|
576
|
+
proposal,
|
|
577
|
+
approver: decision.approver,
|
|
578
|
+
rationale: decision.rationale,
|
|
579
|
+
},
|
|
580
|
+
}));
|
|
581
|
+
return executeApprovedBudgetFallback(taskId, root, proposal, removeUndefined({
|
|
582
|
+
approver: decision.approver,
|
|
583
|
+
rationale: decision.rationale,
|
|
584
|
+
approvalSource: "explicit",
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
async function executeApprovedBudgetFallback(taskId, root, proposal, approval) {
|
|
588
|
+
await appendEvent(root, removeUndefined({
|
|
589
|
+
type: "MODEL_FALLBACK_USED",
|
|
590
|
+
taskId,
|
|
591
|
+
actor: "parent",
|
|
592
|
+
summary: `Approved budget fallback used: ${proposal.toProvider}/${proposal.toModel}`,
|
|
593
|
+
artifacts: proposal.artifact ? [proposal.artifact] : undefined,
|
|
594
|
+
metadata: {
|
|
595
|
+
proposal,
|
|
596
|
+
approver: approval.approver,
|
|
597
|
+
rationale: approval.rationale,
|
|
598
|
+
approvalId: approval.approvalId,
|
|
599
|
+
approvalSource: approval.approvalSource,
|
|
600
|
+
},
|
|
601
|
+
}));
|
|
602
|
+
return executePlanWithFakeProvider(taskId, root, {
|
|
603
|
+
providerId: proposal.toProvider,
|
|
604
|
+
model: proposal.toModel,
|
|
605
|
+
budgetEscalation: removeUndefined({
|
|
606
|
+
status: "approved",
|
|
607
|
+
proposal,
|
|
608
|
+
approver: approval.approver,
|
|
609
|
+
rationale: approval.rationale,
|
|
610
|
+
approvalId: approval.approvalId,
|
|
611
|
+
approvalSource: approval.approvalSource,
|
|
612
|
+
}),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async function enforceLockPreflight(taskId, root) {
|
|
616
|
+
const workspace = await loadWorkspace(root);
|
|
617
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
618
|
+
if (!task) {
|
|
619
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
620
|
+
}
|
|
621
|
+
const locks = locksForTask(task, workspace.locks);
|
|
622
|
+
if (locks.length === 0) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
throw new Error(`task locks active: ${locks.map((lock) => lock.id).join(", ")}`);
|
|
626
|
+
}
|
|
627
|
+
async function enforceDependencyPreflight(taskId, root) {
|
|
628
|
+
const report = await checkTaskDependencies(taskId, root);
|
|
629
|
+
if (report.isSatisfied) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
await appendEvent(root, {
|
|
633
|
+
type: "TASK_DEPENDENCIES_BLOCKED",
|
|
634
|
+
taskId,
|
|
635
|
+
actor: "parent",
|
|
636
|
+
summary: `Task dependencies blocked: ${taskId}`,
|
|
637
|
+
metadata: { report },
|
|
638
|
+
});
|
|
639
|
+
throw new Error(`task dependencies blocked: ${report.incomplete
|
|
640
|
+
.map((dependency) => dependency.id)
|
|
641
|
+
.join(", ")}`);
|
|
642
|
+
}
|
|
643
|
+
function locksForTask(task, locks) {
|
|
644
|
+
return locks.flatMap((lock) => {
|
|
645
|
+
const conflictPath = pathConflict(task.paths ?? [], lock.path);
|
|
646
|
+
if (lock.taskId !== task.id && !conflictPath) {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
return [
|
|
650
|
+
removeUndefined({
|
|
651
|
+
id: lock.id,
|
|
652
|
+
path: lock.path,
|
|
653
|
+
conflictPath,
|
|
654
|
+
ownerRole: lock.ownerRole,
|
|
655
|
+
reason: lock.reason,
|
|
656
|
+
}),
|
|
657
|
+
];
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function pathConflict(paths, lockPath) {
|
|
661
|
+
const normalizedLockPath = normalizePath(lockPath);
|
|
662
|
+
return paths.find((candidate) => pathsConflict(normalizePath(candidate), normalizedLockPath));
|
|
663
|
+
}
|
|
664
|
+
function pathsConflict(taskPath, lockPath) {
|
|
665
|
+
return (taskPath === lockPath ||
|
|
666
|
+
taskPath.startsWith(`${lockPath}/`) ||
|
|
667
|
+
lockPath.startsWith(`${taskPath}/`));
|
|
668
|
+
}
|
|
669
|
+
function normalizePath(value) {
|
|
670
|
+
return value.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
671
|
+
}
|
|
672
|
+
export async function listApprovals(taskId, root = process.cwd()) {
|
|
673
|
+
const approvalDir = resolveWorkflowPath(root, "approvals");
|
|
674
|
+
const fileNames = (await exists(approvalDir))
|
|
675
|
+
? (await readdir(approvalDir)).filter((fileName) => fileName.endsWith(".md"))
|
|
676
|
+
: [];
|
|
677
|
+
const events = await readEvents(root);
|
|
678
|
+
const records = fileNames.map((fileName) => approvalRecordForArtifact(path.join(".agent-workflow", "approvals", fileName), events));
|
|
679
|
+
const filtered = taskId
|
|
680
|
+
? records.filter((record) => record.taskId === taskId)
|
|
681
|
+
: records;
|
|
682
|
+
return filtered.sort((left, right) => left.id.localeCompare(right.id));
|
|
683
|
+
}
|
|
684
|
+
export async function showApproval(id, root = process.cwd()) {
|
|
685
|
+
const record = await getApprovalRecord(id, root);
|
|
686
|
+
const content = await readFile(path.join(root, record.artifact), "utf8");
|
|
687
|
+
return { ...record, content };
|
|
688
|
+
}
|
|
689
|
+
export async function approveApproval(input, root = process.cwd()) {
|
|
690
|
+
return recordApprovalDecision(input, "approved", root);
|
|
691
|
+
}
|
|
692
|
+
export async function rejectApproval(input, root = process.cwd()) {
|
|
693
|
+
return recordApprovalDecision(input, "rejected", root);
|
|
694
|
+
}
|
|
695
|
+
export async function recordReview(input, root = process.cwd()) {
|
|
696
|
+
const workspace = await loadWorkspace(root);
|
|
697
|
+
validateReviewInput(input, workspace.roleIds);
|
|
698
|
+
const fileName = `${input.task}-${input.role}-review.md`;
|
|
699
|
+
const content = [
|
|
700
|
+
`# Review ${input.task}: ${input.role}`,
|
|
701
|
+
"",
|
|
702
|
+
`- Result: ${input.result}`,
|
|
703
|
+
`- Severity: ${input.severity}`,
|
|
704
|
+
`- Findings: ${input.findings}`,
|
|
705
|
+
`- Recommendation: ${input.recommendation}`,
|
|
706
|
+
"",
|
|
707
|
+
].join("\n");
|
|
708
|
+
const artifact = await writeArtifact(root, "reviews", fileName, content);
|
|
709
|
+
await appendEvent(root, {
|
|
710
|
+
type: "REVIEW_RECORDED",
|
|
711
|
+
taskId: input.task,
|
|
712
|
+
actor: input.role,
|
|
713
|
+
summary: `Review recorded: ${input.result}`,
|
|
714
|
+
artifacts: [artifact],
|
|
715
|
+
metadata: { result: input.result, severity: input.severity },
|
|
716
|
+
});
|
|
717
|
+
return { artifact, content };
|
|
718
|
+
}
|
|
719
|
+
function riskReviewSteps(impactAreas) {
|
|
720
|
+
const roleByImpact = {
|
|
721
|
+
security: "security",
|
|
722
|
+
sre: "sre",
|
|
723
|
+
dba: "dba",
|
|
724
|
+
devops: "devops",
|
|
725
|
+
compliance: "compliance_privacy",
|
|
726
|
+
ux: "ux_ui_designer",
|
|
727
|
+
};
|
|
728
|
+
const roles = impactAreas
|
|
729
|
+
.map((area) => roleByImpact[area])
|
|
730
|
+
.filter((role) => Boolean(role));
|
|
731
|
+
return [...new Set(roles)].map((role) => ({
|
|
732
|
+
id: `risk-${role}`,
|
|
733
|
+
role,
|
|
734
|
+
action: "Review risk impact and approve, block, or record accepted risk.",
|
|
735
|
+
dependsOn: ["development"],
|
|
736
|
+
gate: "risk-review",
|
|
737
|
+
}));
|
|
738
|
+
}
|
|
739
|
+
async function writeRunArtifact(root, taskId, stepId, content) {
|
|
740
|
+
await ensureDir(resolveWorkflowPath(root, "runs", taskId));
|
|
741
|
+
return writeArtifact(root, path.join("runs", taskId), `${stepId}.md`, content);
|
|
742
|
+
}
|
|
743
|
+
async function recordTaskGraphBatchRun(batch, root) {
|
|
744
|
+
const artifact = await writeTaskGraphBatchArtifact(root, batch);
|
|
745
|
+
const result = removeUndefined({
|
|
746
|
+
...batch,
|
|
747
|
+
artifact,
|
|
748
|
+
});
|
|
749
|
+
await appendEvent(root, {
|
|
750
|
+
type: batch.failure ? "ORCH_BATCH_FAILED" : "ORCH_BATCH_COMPLETED",
|
|
751
|
+
actor: "parent",
|
|
752
|
+
summary: batch.failure
|
|
753
|
+
? `Task graph batch failed: ${batch.failure.taskId}`
|
|
754
|
+
: "Task graph batch completed",
|
|
755
|
+
artifacts: [artifact],
|
|
756
|
+
metadata: {
|
|
757
|
+
selectedTaskIds: batch.selectedTaskIds,
|
|
758
|
+
completedTaskIds: batch.runs.map((run) => run.taskId),
|
|
759
|
+
failure: batch.failure,
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
async function writeTaskGraphBatchArtifact(root, batch) {
|
|
765
|
+
const timestamp = new Date().toISOString();
|
|
766
|
+
await ensureDir(resolveWorkflowPath(root, "runs", "batches"));
|
|
767
|
+
return writeArtifact(root, path.join("runs", "batches"), `${timestamp.replace(/[:.]/g, "-")}-graph-batch.md`, [
|
|
768
|
+
"# Task Graph Batch Run",
|
|
769
|
+
"",
|
|
770
|
+
`- Timestamp: ${timestamp}`,
|
|
771
|
+
`- Selected tasks: ${batch.selectedTaskIds.join(", ")}`,
|
|
772
|
+
`- Completed runs: ${batch.runs.length}`,
|
|
773
|
+
`- Locked tasks: ${batch.locked.map((task) => task.id).join(", ") || "none"}`,
|
|
774
|
+
`- Failure: ${batch.failure
|
|
775
|
+
? `${batch.failure.taskId}: ${batch.failure.error}`
|
|
776
|
+
: "none"}`,
|
|
777
|
+
"",
|
|
778
|
+
"## Runs",
|
|
779
|
+
...(batch.runs.length === 0
|
|
780
|
+
? ["- none"]
|
|
781
|
+
: batch.runs.map((run) => `- ${run.taskId}: ${run.steps.length} steps via ${run.provider}/${run.model}`)),
|
|
782
|
+
"",
|
|
783
|
+
].join("\n"));
|
|
784
|
+
}
|
|
785
|
+
export async function listReviews(taskId, root = process.cwd()) {
|
|
786
|
+
return filterEvents("REVIEW_RECORDED", taskId, root);
|
|
787
|
+
}
|
|
788
|
+
export async function addEvidence(input, root = process.cwd()) {
|
|
789
|
+
const workspace = await loadWorkspace(root);
|
|
790
|
+
validateEvidenceInput(input, workspace.roleIds);
|
|
791
|
+
if (typeof input.path === "string" &&
|
|
792
|
+
!(await exists(path.resolve(root, input.path)))) {
|
|
793
|
+
throw new Error(`evidence path does not exist: ${input.path}`);
|
|
794
|
+
}
|
|
795
|
+
const fileName = `${input.task}-${Date.now()}-${input.type}.md`;
|
|
796
|
+
const content = [
|
|
797
|
+
`# Evidence ${input.task}: ${input.type}`,
|
|
798
|
+
"",
|
|
799
|
+
`- Role: ${input.role}`,
|
|
800
|
+
`- Summary: ${input.summary}`,
|
|
801
|
+
`- Path: ${input.path ?? "not applicable"}`,
|
|
802
|
+
`- Command: ${input.command ?? "not applicable"}`,
|
|
803
|
+
`- Exit code: ${input.exitCode ?? "not applicable"}`,
|
|
804
|
+
"",
|
|
805
|
+
].join("\n");
|
|
806
|
+
const artifact = await writeArtifact(root, "evidence", fileName, content);
|
|
807
|
+
await appendEvent(root, {
|
|
808
|
+
type: "EVIDENCE_ADDED",
|
|
809
|
+
taskId: input.task,
|
|
810
|
+
actor: input.role,
|
|
811
|
+
summary: input.summary,
|
|
812
|
+
artifacts: [artifact],
|
|
813
|
+
metadata: { type: input.type },
|
|
814
|
+
});
|
|
815
|
+
return { artifact, content };
|
|
816
|
+
}
|
|
817
|
+
export async function listEvidence(taskId, root = process.cwd()) {
|
|
818
|
+
return filterEvents("EVIDENCE_ADDED", taskId, root);
|
|
819
|
+
}
|
|
820
|
+
export async function getWorkflowSummary(root = process.cwd()) {
|
|
821
|
+
const workspace = await loadWorkspace(root);
|
|
822
|
+
const events = await readEvents(root);
|
|
823
|
+
return {
|
|
824
|
+
tasks: workspace.tasks,
|
|
825
|
+
locks: workspace.locks,
|
|
826
|
+
reviews: events.filter((event) => event.type === "REVIEW_RECORDED"),
|
|
827
|
+
evidence: events.filter((event) => event.type === "EVIDENCE_ADDED"),
|
|
828
|
+
blockers: workspace.tasks.filter((task) => task.status === "blocked"),
|
|
829
|
+
eventCount: events.length,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
export async function generatePullRequestSummary(taskId, root = process.cwd()) {
|
|
833
|
+
const workspace = await loadWorkspace(root);
|
|
834
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
835
|
+
if (!task) {
|
|
836
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
837
|
+
}
|
|
838
|
+
const events = await readEvents(root);
|
|
839
|
+
const taskEvents = events.filter((event) => event.taskId === taskId);
|
|
840
|
+
return {
|
|
841
|
+
task,
|
|
842
|
+
handoffs: taskEvents.filter((event) => event.type === "HANDOFF_READY"),
|
|
843
|
+
reviews: taskEvents.filter((event) => event.type === "REVIEW_RECORDED"),
|
|
844
|
+
evidence: taskEvents.filter((event) => event.type === "EVIDENCE_ADDED"),
|
|
845
|
+
gates: taskEvents.filter((event) => event.type === "GATE_PASSED" || event.type === "GATE_BLOCKED"),
|
|
846
|
+
locks: workspace.locks.filter((lock) => lock.taskId === taskId),
|
|
847
|
+
risks: task.risks ?? [],
|
|
848
|
+
rollout: "Use the project release process after required gates pass.",
|
|
849
|
+
rollback: "Revert the related commit or disable the changed behavior.",
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
export async function generatePlaywrightTestPlan(taskId, root = process.cwd()) {
|
|
853
|
+
const workspace = await loadWorkspace(root);
|
|
854
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
855
|
+
if (!task) {
|
|
856
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
857
|
+
}
|
|
858
|
+
const criteria = task.acceptanceCriteria && task.acceptanceCriteria.length > 0
|
|
859
|
+
? task.acceptanceCriteria
|
|
860
|
+
: [task.goal ?? task.title];
|
|
861
|
+
return {
|
|
862
|
+
taskId: task.id,
|
|
863
|
+
title: task.title,
|
|
864
|
+
targetUser: "end user",
|
|
865
|
+
scenarios: criteria.map((criterion, index) => ({
|
|
866
|
+
name: `Scenario ${index + 1}: ${criterion}`,
|
|
867
|
+
source: criterion,
|
|
868
|
+
pageObject: `${toPascalCase(task.id)}Page`,
|
|
869
|
+
selectors: [
|
|
870
|
+
"Use role, label, and test-id selectors before CSS selectors",
|
|
871
|
+
"Keep selectors in page object methods",
|
|
872
|
+
],
|
|
873
|
+
assertions: [
|
|
874
|
+
`Verify user-visible outcome: ${criterion}`,
|
|
875
|
+
"Verify no blocking error state is shown",
|
|
876
|
+
],
|
|
877
|
+
evidence: ["screenshot", "trace-on-failure"],
|
|
878
|
+
})),
|
|
879
|
+
fixtures: ["authenticated user when the flow requires identity"],
|
|
880
|
+
notes: [
|
|
881
|
+
"Prefer mobile-first viewport coverage before desktop expansion",
|
|
882
|
+
"Attach trace, screenshot, or video when failures occur",
|
|
883
|
+
],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
export async function getWorkflowConfig(root = process.cwd()) {
|
|
887
|
+
return readJson(resolveWorkflowPath(root, FILES.config), {});
|
|
888
|
+
}
|
|
889
|
+
export async function listConfiguredModelProviders(root = process.cwd()) {
|
|
890
|
+
return summarizeConfiguredProviders(await getWorkflowConfig(root));
|
|
891
|
+
}
|
|
892
|
+
export async function completeWithProviderFallback(routing, prompt, { failingProviders = [], root = process.cwd(), taskId, role = "parent", } = {}) {
|
|
893
|
+
const registry = new InMemoryModelProviderRegistry();
|
|
894
|
+
const providerIds = [routing.provider, ...routing.fallbacks];
|
|
895
|
+
for (const providerId of providerIds) {
|
|
896
|
+
registry.register(new FakeModelProvider({
|
|
897
|
+
id: providerId,
|
|
898
|
+
shouldFail: failingProviders.includes(providerId),
|
|
899
|
+
}));
|
|
900
|
+
}
|
|
901
|
+
const failedProviders = [];
|
|
902
|
+
for (const [index, providerId] of providerIds.entries()) {
|
|
903
|
+
try {
|
|
904
|
+
const provider = registry.get(providerId);
|
|
905
|
+
const response = await provider.complete({
|
|
906
|
+
model: routing.model,
|
|
907
|
+
messages: [{ role: "user", content: prompt }],
|
|
908
|
+
});
|
|
909
|
+
if (index > 0) {
|
|
910
|
+
await appendEvent(root, removeUndefined({
|
|
911
|
+
type: "MODEL_FALLBACK_USED",
|
|
912
|
+
actor: role,
|
|
913
|
+
taskId,
|
|
914
|
+
summary: `Fallback provider used: ${providerId}`,
|
|
915
|
+
metadata: { provider: providerId, failedProviders },
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
provider: providerId,
|
|
920
|
+
model: routing.model,
|
|
921
|
+
response,
|
|
922
|
+
fallbackUsed: index > 0,
|
|
923
|
+
failedProviders,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
failedProviders.push({
|
|
928
|
+
provider: providerId,
|
|
929
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
throw new Error(`all providers failed: ${failedProviders
|
|
934
|
+
.map((failure) => failure.provider)
|
|
935
|
+
.join(", ")}`);
|
|
936
|
+
}
|
|
937
|
+
export async function setRoleModelProvider(role, routing, root = process.cwd()) {
|
|
938
|
+
const workspace = await loadWorkspace(root);
|
|
939
|
+
if (!workspace.roleIds.has(role)) {
|
|
940
|
+
throw new Error(`unknown role: ${role}`);
|
|
941
|
+
}
|
|
942
|
+
const configPath = resolveWorkflowPath(root, FILES.config);
|
|
943
|
+
const config = await readJson(configPath, {});
|
|
944
|
+
config.providers = {
|
|
945
|
+
defaults: config.providers.defaults,
|
|
946
|
+
byRole: {
|
|
947
|
+
...(config.providers.byRole ?? {}),
|
|
948
|
+
[role]: routing,
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
await writeJson(configPath, config);
|
|
952
|
+
await appendEvent(root, {
|
|
953
|
+
type: "MODEL_ROUTING_CONFIGURED",
|
|
954
|
+
actor: "parent",
|
|
955
|
+
summary: `Model routing configured for ${role}`,
|
|
956
|
+
metadata: { role, provider: routing.provider, model: routing.model },
|
|
957
|
+
});
|
|
958
|
+
return routing;
|
|
959
|
+
}
|
|
960
|
+
export async function recordModelProvenance(input, root = process.cwd()) {
|
|
961
|
+
const workspace = await loadWorkspace(root);
|
|
962
|
+
if (!workspace.tasks.some((task) => task.id === input.task)) {
|
|
963
|
+
throw new Error(`unknown task: ${input.task}`);
|
|
964
|
+
}
|
|
965
|
+
if (!workspace.roleIds.has(input.role)) {
|
|
966
|
+
throw new Error(`unknown role: ${input.role}`);
|
|
967
|
+
}
|
|
968
|
+
const timestamp = new Date().toISOString();
|
|
969
|
+
const record = { ...input, timestamp };
|
|
970
|
+
await appendEvent(root, {
|
|
971
|
+
type: "MODEL_PROVENANCE_RECORDED",
|
|
972
|
+
taskId: input.task,
|
|
973
|
+
actor: input.role,
|
|
974
|
+
summary: `Model provenance recorded for ${input.provider}/${input.model}`,
|
|
975
|
+
metadata: { ...record },
|
|
976
|
+
});
|
|
977
|
+
return record;
|
|
978
|
+
}
|
|
979
|
+
export async function listModelProvenance(taskId, root = process.cwd()) {
|
|
980
|
+
const events = await filterEvents("MODEL_PROVENANCE_RECORDED", taskId, root);
|
|
981
|
+
return events.map((event) => event.metadata);
|
|
982
|
+
}
|
|
983
|
+
export async function getUsageReport(taskId, root = process.cwd()) {
|
|
984
|
+
const records = await listModelProvenance(taskId, root);
|
|
985
|
+
return {
|
|
986
|
+
...(taskId ? { taskId } : {}),
|
|
987
|
+
totals: aggregateUsage("total", records),
|
|
988
|
+
byRole: aggregateUsageBy(records, (record) => record.role),
|
|
989
|
+
byProvider: aggregateUsageBy(records, (record) => record.provider),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
export async function checkUsageBudget(taskId, root = process.cwd()) {
|
|
993
|
+
const config = await readJson(resolveWorkflowPath(root, FILES.config), {});
|
|
994
|
+
if (taskId) {
|
|
995
|
+
const workspace = await loadWorkspace(root);
|
|
996
|
+
if (!workspace.tasks.some((task) => task.id === taskId)) {
|
|
997
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const usage = await getUsageReport(taskId, root);
|
|
1001
|
+
const budgets = config.budgets;
|
|
1002
|
+
const appliedBudgets = [];
|
|
1003
|
+
const violations = [];
|
|
1004
|
+
if (budgets?.defaults) {
|
|
1005
|
+
appliedBudgets.push("defaults");
|
|
1006
|
+
violations.push(...budgetViolations("defaults", usage.totals, budgets.defaults));
|
|
1007
|
+
}
|
|
1008
|
+
if (taskId && budgets?.byTask?.[taskId]) {
|
|
1009
|
+
appliedBudgets.push(`task:${taskId}`);
|
|
1010
|
+
violations.push(...budgetViolations(`task:${taskId}`, usage.totals, budgets.byTask[taskId]));
|
|
1011
|
+
}
|
|
1012
|
+
for (const roleUsage of usage.byRole) {
|
|
1013
|
+
const roleBudget = budgets?.byRole?.[roleUsage.key];
|
|
1014
|
+
if (roleBudget) {
|
|
1015
|
+
appliedBudgets.push(`role:${roleUsage.key}`);
|
|
1016
|
+
violations.push(...budgetViolations(`role:${roleUsage.key}`, roleUsage, roleBudget));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
...(taskId ? { taskId } : {}),
|
|
1021
|
+
passed: violations.length === 0,
|
|
1022
|
+
usage,
|
|
1023
|
+
appliedBudgets,
|
|
1024
|
+
violations,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
async function createBudgetFallbackProposal(taskId, budget, root) {
|
|
1028
|
+
const workspace = await loadWorkspace(root);
|
|
1029
|
+
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
1030
|
+
if (!task) {
|
|
1031
|
+
throw new Error(`unknown task: ${taskId}`);
|
|
1032
|
+
}
|
|
1033
|
+
const config = await readJson(resolveWorkflowPath(root, FILES.config), {});
|
|
1034
|
+
const routing = config.providers.byRole?.[task.ownerRole] ?? config.providers.defaults;
|
|
1035
|
+
const fallbackProvider = routing.fallbacks[0];
|
|
1036
|
+
if (!fallbackProvider) {
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
const proposal = {
|
|
1040
|
+
fromProvider: routing.provider,
|
|
1041
|
+
fromModel: routing.model,
|
|
1042
|
+
toProvider: fallbackProvider,
|
|
1043
|
+
toModel: routing.model,
|
|
1044
|
+
violations: budget.violations,
|
|
1045
|
+
};
|
|
1046
|
+
const artifact = await writeBudgetEscalationProposal(root, taskId, proposal);
|
|
1047
|
+
return {
|
|
1048
|
+
...proposal,
|
|
1049
|
+
artifact,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
async function getApprovalRecord(id, root) {
|
|
1053
|
+
const artifact = approvalArtifactForId(id);
|
|
1054
|
+
if (!(await exists(path.join(root, artifact)))) {
|
|
1055
|
+
throw new Error(`unknown approval: ${id}`);
|
|
1056
|
+
}
|
|
1057
|
+
const events = await readEvents(root);
|
|
1058
|
+
return approvalRecordForArtifact(artifact, events);
|
|
1059
|
+
}
|
|
1060
|
+
async function findStoredApprovalForProposal(proposal, root) {
|
|
1061
|
+
if (!proposal.artifact) {
|
|
1062
|
+
return undefined;
|
|
1063
|
+
}
|
|
1064
|
+
const events = await readEvents(root);
|
|
1065
|
+
return approvalRecordForArtifact(proposal.artifact, events);
|
|
1066
|
+
}
|
|
1067
|
+
async function recordApprovalDecision(input, status, root) {
|
|
1068
|
+
const current = await getApprovalRecord(input.id, root);
|
|
1069
|
+
const requested = approvalEventsForArtifact(await readEvents(root), current.artifact).find((event) => event.type === "BUDGET_ESCALATION_REQUESTED");
|
|
1070
|
+
await appendEvent(root, removeUndefined({
|
|
1071
|
+
type: status === "approved"
|
|
1072
|
+
? "BUDGET_ESCALATION_APPROVED"
|
|
1073
|
+
: "BUDGET_ESCALATION_REJECTED",
|
|
1074
|
+
taskId: current.taskId,
|
|
1075
|
+
actor: "parent",
|
|
1076
|
+
summary: `Budget escalation ${status}: ${input.id}`,
|
|
1077
|
+
artifacts: [current.artifact],
|
|
1078
|
+
metadata: {
|
|
1079
|
+
approvalId: input.id,
|
|
1080
|
+
proposal: requested?.metadata.proposal,
|
|
1081
|
+
approver: input.approver,
|
|
1082
|
+
rationale: input.rationale,
|
|
1083
|
+
},
|
|
1084
|
+
}));
|
|
1085
|
+
return getApprovalRecord(input.id, root);
|
|
1086
|
+
}
|
|
1087
|
+
function approvalRecordForArtifact(artifact, events) {
|
|
1088
|
+
const id = approvalIdForArtifact(artifact);
|
|
1089
|
+
const related = approvalEventsForArtifact(events, artifact);
|
|
1090
|
+
const requested = related.find((event) => event.type === "BUDGET_ESCALATION_REQUESTED");
|
|
1091
|
+
const decision = [...related]
|
|
1092
|
+
.reverse()
|
|
1093
|
+
.find((event) => ["BUDGET_ESCALATION_APPROVED", "BUDGET_ESCALATION_REJECTED"].includes(event.type));
|
|
1094
|
+
const status = decisionStatus(decision);
|
|
1095
|
+
return removeUndefined({
|
|
1096
|
+
id,
|
|
1097
|
+
taskId: String(requested?.taskId ?? decision?.taskId ?? "") || undefined,
|
|
1098
|
+
artifact,
|
|
1099
|
+
status,
|
|
1100
|
+
summary: requested?.summary ?? `Approval proposal ${id}`,
|
|
1101
|
+
requestedAt: requested?.timestamp,
|
|
1102
|
+
decidedAt: decision?.timestamp,
|
|
1103
|
+
approver: typeof decision?.metadata.approver === "string"
|
|
1104
|
+
? decision.metadata.approver
|
|
1105
|
+
: undefined,
|
|
1106
|
+
rationale: typeof decision?.metadata.rationale === "string"
|
|
1107
|
+
? decision.metadata.rationale
|
|
1108
|
+
: undefined,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
function approvalEventsForArtifact(events, artifact) {
|
|
1112
|
+
return events.filter((event) => event.artifacts?.includes(artifact));
|
|
1113
|
+
}
|
|
1114
|
+
function decisionStatus(decision) {
|
|
1115
|
+
if (decision?.type === "BUDGET_ESCALATION_APPROVED") {
|
|
1116
|
+
return "approved";
|
|
1117
|
+
}
|
|
1118
|
+
if (decision?.type === "BUDGET_ESCALATION_REJECTED") {
|
|
1119
|
+
return "rejected";
|
|
1120
|
+
}
|
|
1121
|
+
return "pending";
|
|
1122
|
+
}
|
|
1123
|
+
function approvalArtifactForId(id) {
|
|
1124
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(id)) {
|
|
1125
|
+
throw new Error(`invalid approval id: ${id}`);
|
|
1126
|
+
}
|
|
1127
|
+
return path.join(".agent-workflow", "approvals", `${id}.md`);
|
|
1128
|
+
}
|
|
1129
|
+
function approvalIdForArtifact(artifact) {
|
|
1130
|
+
return path.basename(artifact, ".md");
|
|
1131
|
+
}
|
|
1132
|
+
export async function addPlaywrightEvidence(input, root = process.cwd()) {
|
|
1133
|
+
return addEvidence(removeUndefined({
|
|
1134
|
+
task: input.task,
|
|
1135
|
+
role: "qa",
|
|
1136
|
+
type: input.kind,
|
|
1137
|
+
summary: input.summary,
|
|
1138
|
+
path: input.path,
|
|
1139
|
+
command: input.runId,
|
|
1140
|
+
}), root);
|
|
1141
|
+
}
|
|
1142
|
+
function aggregateUsage(key, records) {
|
|
1143
|
+
const inputTokens = records.reduce((sum, record) => sum + record.inputTokens, 0);
|
|
1144
|
+
const outputTokens = records.reduce((sum, record) => sum + record.outputTokens, 0);
|
|
1145
|
+
const estimatedCostUsd = records.reduce((sum, record) => sum + record.estimatedCostUsd, 0);
|
|
1146
|
+
return {
|
|
1147
|
+
key,
|
|
1148
|
+
requests: records.length,
|
|
1149
|
+
inputTokens,
|
|
1150
|
+
outputTokens,
|
|
1151
|
+
totalTokens: inputTokens + outputTokens,
|
|
1152
|
+
estimatedCostUsd,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function aggregateUsageBy(records, keyFor) {
|
|
1156
|
+
const groups = new Map();
|
|
1157
|
+
for (const record of records) {
|
|
1158
|
+
const key = keyFor(record);
|
|
1159
|
+
groups.set(key, [...(groups.get(key) ?? []), record]);
|
|
1160
|
+
}
|
|
1161
|
+
return [...groups.entries()].map(([key, group]) => aggregateUsage(key, group));
|
|
1162
|
+
}
|
|
1163
|
+
function budgetViolations(scope, usage, budget) {
|
|
1164
|
+
return [
|
|
1165
|
+
budgetViolation(scope, "requests", usage.requests, budget.maxRequests),
|
|
1166
|
+
budgetViolation(scope, "inputTokens", usage.inputTokens, budget.maxInputTokens),
|
|
1167
|
+
budgetViolation(scope, "outputTokens", usage.outputTokens, budget.maxOutputTokens),
|
|
1168
|
+
budgetViolation(scope, "totalTokens", usage.totalTokens, budget.maxTotalTokens),
|
|
1169
|
+
budgetViolation(scope, "estimatedCostUsd", usage.estimatedCostUsd, budget.maxEstimatedCostUsd),
|
|
1170
|
+
].filter((violation) => Boolean(violation));
|
|
1171
|
+
}
|
|
1172
|
+
function budgetViolation(scope, metric, actual, limit) {
|
|
1173
|
+
if (limit === undefined || actual <= limit) {
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
scope,
|
|
1178
|
+
metric,
|
|
1179
|
+
actual,
|
|
1180
|
+
limit,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
async function writeBudgetEscalationProposal(root, taskId, proposal) {
|
|
1184
|
+
const content = [
|
|
1185
|
+
`# Budget Escalation Proposal: ${taskId}`,
|
|
1186
|
+
"",
|
|
1187
|
+
"## Summary",
|
|
1188
|
+
"- Status: approval required before execution",
|
|
1189
|
+
"- Usage source: local model provenance events",
|
|
1190
|
+
"- Cost source: estimated cost recorded in provenance",
|
|
1191
|
+
"",
|
|
1192
|
+
"## Current Routing",
|
|
1193
|
+
`- Provider: ${proposal.fromProvider}`,
|
|
1194
|
+
`- Model: ${proposal.fromModel}`,
|
|
1195
|
+
"",
|
|
1196
|
+
"## Proposed Fallback",
|
|
1197
|
+
`- Provider: ${proposal.toProvider}`,
|
|
1198
|
+
`- Model: ${proposal.toModel}`,
|
|
1199
|
+
"",
|
|
1200
|
+
"## Budget Violations",
|
|
1201
|
+
...proposal.violations.map((violation) => `- ${violation.scope} ${violation.metric}: ${violation.actual} > ${violation.limit}`),
|
|
1202
|
+
"",
|
|
1203
|
+
"## Expected Impact",
|
|
1204
|
+
"- May continue execution using the configured fallback provider.",
|
|
1205
|
+
"- Quality, latency, and cost can differ from the original provider.",
|
|
1206
|
+
"- Approval applies only to this execution request.",
|
|
1207
|
+
"",
|
|
1208
|
+
"## Risks",
|
|
1209
|
+
"- Estimated usage may differ from provider invoice totals.",
|
|
1210
|
+
"- Fallback output quality must still pass QA and release gates.",
|
|
1211
|
+
"- No raw prompts, raw responses, or secrets are included in this proposal.",
|
|
1212
|
+
"",
|
|
1213
|
+
"## Approval Commands",
|
|
1214
|
+
`- Approve: orchestra run --task ${taskId} --approve-budget-fallback --approver <name> --rationale "<reason>"`,
|
|
1215
|
+
`- Reject: orchestra run --task ${taskId} --reject-budget-fallback --approver <name> --rationale "<reason>"`,
|
|
1216
|
+
"",
|
|
1217
|
+
].join("\n");
|
|
1218
|
+
return writeArtifact(root, "approvals", `${taskId}-budget-fallback.md`, content);
|
|
1219
|
+
}
|
|
1220
|
+
async function filterEvents(type, taskId, root) {
|
|
1221
|
+
const events = (await readEvents(root)).filter((event) => event.type === type);
|
|
1222
|
+
return taskId ? events.filter((event) => event.taskId === taskId) : events;
|
|
1223
|
+
}
|
|
1224
|
+
async function writeTasks(base, tasks) {
|
|
1225
|
+
await writeJson(path.join(base, FILES.tasks), tasks);
|
|
1226
|
+
}
|
|
1227
|
+
async function writeLocks(base, locks) {
|
|
1228
|
+
await writeJson(path.join(base, FILES.locks), locks);
|
|
1229
|
+
}
|
|
1230
|
+
function removeUndefined(value) {
|
|
1231
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1232
|
+
}
|
|
1233
|
+
function toPascalCase(value) {
|
|
1234
|
+
return value
|
|
1235
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
1236
|
+
.filter(Boolean)
|
|
1237
|
+
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
1238
|
+
.join("");
|
|
1239
|
+
}
|
|
1240
|
+
//# sourceMappingURL=workflow-services.js.map
|