@tea-agent/loop-agent 0.1.0 → 0.2.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/CHANGELOG.md +17 -1
- package/README.md +127 -92
- package/dist/adapters/index.js +3 -2
- package/dist/adapters/loop-agent.js +44 -2
- package/dist/application/dag/args.js +420 -0
- package/dist/application/dag/generate-task-dag.js +280 -0
- package/dist/application/dag/report-dag.js +14 -0
- package/dist/application/dag/run-dag.js +93 -0
- package/dist/application/dag/validate-dag.js +101 -0
- package/dist/application/loop/run-action.js +23 -0
- package/dist/cli/catalog.js +2 -237
- package/dist/cli/command-definitions.js +571 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/program.js +65 -1
- package/dist/cli/router.js +13 -0
- package/dist/cli-governance/active-residue-check.js +38 -0
- package/dist/commands/dag-report.js +6 -107
- package/dist/commands/dag-run-task.js +8 -466
- package/dist/commands/dag-validate.js +7 -179
- package/dist/commands/examples.js +90 -0
- package/dist/commands/init.js +1495 -0
- package/dist/commands/loop.js +57 -31
- package/dist/commands/pi-prompt.js +2 -9
- package/dist/commands/run-dag.js +7 -180
- package/dist/executors/cursor-executor-artifacts.js +3 -4
- package/dist/executors/cursor-worker-client.js +13 -3
- package/dist/executors/dag-cursor-executor.js +2 -3
- package/dist/executors/dag-pi-executor.js +3 -4
- package/dist/executors/dag-static-executor.js +2 -5
- package/dist/executors/pi-defaults.js +9 -0
- package/dist/executors/shell-executor.js +12 -20
- package/dist/governance/manifest-types.js +1 -0
- package/dist/infrastructure/harness/active-residue-policy.js +73 -0
- package/dist/infrastructure/harness/artifact-store.js +72 -0
- package/dist/infrastructure/harness/atomic-write.js +49 -0
- package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
- package/dist/infrastructure/harness/loop-action-store.js +23 -0
- package/dist/infrastructure/harness/loop-store.js +41 -0
- package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
- package/dist/infrastructure/harness/task-store.js +77 -0
- package/dist/records/one-shot-runs.js +26 -61
- package/dist/records/promotion.js +3 -4
- package/dist/shared/artifacts-core.js +5 -5
- package/dist/shared/logger.js +9 -15
- package/dist/task/delegate.js +4 -4
- package/dist/task/runtime.js +5 -7
- package/dist/task/state.js +6 -20
- package/dist/workflows/dag/convergence/controller.js +277 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
- package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
- package/dist/workflows/dag/lifecycle.js +6 -5
- package/dist/workflows/dag/node-execution.js +262 -0
- package/dist/workflows/dag/run-store.js +36 -0
- package/dist/workflows/dag/runner.js +82 -1341
- package/dist/workflows/dag/scheduler.js +84 -0
- package/dist/workflows/dag/upstream-artifacts.js +20 -18
- package/dist/workflows/loop/actions/cursor-fix.js +191 -0
- package/dist/workflows/loop/actions/dag-action.js +130 -0
- package/dist/workflows/loop/actions/pi-review.js +267 -0
- package/dist/workflows/loop/actions/shared.js +157 -0
- package/dist/workflows/loop/actions/shell-verify.js +82 -0
- package/dist/workflows/loop/actions/types.js +1 -0
- package/dist/workflows/loop/actions/workflow-action.js +255 -0
- package/dist/workflows/loop/actions.js +55 -1212
- package/dist/workflows/loop/closeout.js +5 -4
- package/dist/workflows/loop/context.js +2 -3
- package/dist/workflows/loop/events.js +3 -2
- package/dist/workflows/loop/policy/auto-policy.js +104 -0
- package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
- package/dist/workflows/loop/rounds.js +3 -3
- package/dist/workflows/loop/signals.js +4 -7
- package/dist/workflows/loop/state.js +11 -11
- package/docs/README.md +3 -2
- package/docs/architecture/runtime-boundaries.md +147 -0
- package/docs/exec-plans/active/README.md +4 -0
- package/docs/exec-plans/completed/README.md +6 -2
- package/package.json +2 -1
- package/skills/ai-engineering-context/SKILL.md +21 -21
- package/skills/loop-agent/SKILL.md +73 -188
- package/skills/loop-agent/references/README.md +6 -2
- package/skills/loop-agent/references/harness-policy.md +113 -113
- package/skills/loop-agent/references/learned/README.md +13 -13
- package/skills/loop-agent/references/long-running-loop.md +59 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
- package/skills/loop-agent/references/verification-and-failure-handling.md +18 -0
- package/skills/requesting-code-review/SKILL.md +40 -40
- package/skills/requesting-code-review/code-reviewer.md +4 -4
- package/skills/systematic-debugging/CREATION-LOG.md +43 -43
- package/skills/systematic-debugging/SKILL.md +113 -113
- package/skills/systematic-debugging/condition-based-waiting.md +20 -20
- package/skills/systematic-debugging/defense-in-depth.md +27 -27
- package/skills/systematic-debugging/root-cause-tracing.md +38 -38
- package/skills/systematic-debugging/test-academic.md +6 -6
- package/skills/systematic-debugging/test-pressure-1.md +6 -6
- package/skills/systematic-debugging/test-pressure-2.md +2 -2
- package/skills/systematic-debugging/test-pressure-3.md +6 -6
- package/skills/verification-before-completion/SKILL.md +37 -37
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { DAG_PAUSED_LATEST_REPORT_PRESET, DAG_RECOVERY_ACTIONS, } from "../../workflows/dag/report.js";
|
|
3
|
+
const RECOVERY_ACTIONS = new Set(DAG_RECOVERY_ACTIONS);
|
|
4
|
+
const LIFECYCLE_FILTERS = new Set([
|
|
5
|
+
"active",
|
|
6
|
+
"paused",
|
|
7
|
+
"completed",
|
|
8
|
+
"all",
|
|
9
|
+
]);
|
|
10
|
+
export function parseDagRunTaskArgs(args, defaultCwd) {
|
|
11
|
+
if (args.length === 0) {
|
|
12
|
+
throw new Error("usage: dag run-task <task-id> [--output <path>] [--profile auto|minimal|standard|reviewed|supervised] [--strict-models] [--no-cursor] [--execute] [--init-only] [--dry-run] [--cwd <dir>] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]]");
|
|
13
|
+
}
|
|
14
|
+
let taskId;
|
|
15
|
+
let outputPath;
|
|
16
|
+
let strictModels = false;
|
|
17
|
+
let noCursor = false;
|
|
18
|
+
let execute = false;
|
|
19
|
+
let initOnly = false;
|
|
20
|
+
let dryRun = false;
|
|
21
|
+
let cwd;
|
|
22
|
+
let maxConcurrent;
|
|
23
|
+
let runId;
|
|
24
|
+
let canvasPath;
|
|
25
|
+
let canvasName;
|
|
26
|
+
let canvasesDir;
|
|
27
|
+
let profile = "standard";
|
|
28
|
+
let profileExplicit = false;
|
|
29
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === "--output" || arg === "-o") {
|
|
32
|
+
outputPath = args[++i];
|
|
33
|
+
if (!outputPath) {
|
|
34
|
+
throw new Error("dag run-task --output requires a path");
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg.startsWith("--output=")) {
|
|
39
|
+
outputPath = arg.slice("--output=".length);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === "--strict-models") {
|
|
43
|
+
strictModels = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg === "--no-cursor") {
|
|
47
|
+
noCursor = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg === "--execute") {
|
|
51
|
+
execute = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--init-only") {
|
|
55
|
+
initOnly = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === "--dry-run") {
|
|
59
|
+
dryRun = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === "--cwd" || arg === "-C") {
|
|
63
|
+
cwd = args[++i];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg.startsWith("--cwd=")) {
|
|
67
|
+
cwd = arg.slice(6);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--max-concurrent") {
|
|
71
|
+
maxConcurrent = Number(args[++i]);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg === "--run-id") {
|
|
75
|
+
runId = args[++i];
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--canvas-path") {
|
|
79
|
+
canvasPath = args[++i];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg.startsWith("--canvas-path=")) {
|
|
83
|
+
canvasPath = arg.slice(14);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--canvas") {
|
|
87
|
+
canvasName = args[++i];
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg.startsWith("--canvas=")) {
|
|
91
|
+
canvasName = arg.slice(9);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === "--canvases-dir") {
|
|
95
|
+
canvasesDir = args[++i];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg.startsWith("--canvases-dir=")) {
|
|
99
|
+
canvasesDir = arg.slice(15);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (arg === "--profile") {
|
|
103
|
+
const value = args[++i];
|
|
104
|
+
if (!value) {
|
|
105
|
+
throw new Error("dag run-task --profile requires a value");
|
|
106
|
+
}
|
|
107
|
+
profile = parseDagRunTaskProfile(value);
|
|
108
|
+
profileExplicit = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (arg.startsWith("--profile=")) {
|
|
112
|
+
profile = parseDagRunTaskProfile(arg.slice("--profile=".length));
|
|
113
|
+
profileExplicit = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (arg.startsWith("-")) {
|
|
117
|
+
throw new Error(`unknown dag run-task flag: ${arg}`);
|
|
118
|
+
}
|
|
119
|
+
if (taskId) {
|
|
120
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
121
|
+
}
|
|
122
|
+
taskId = arg;
|
|
123
|
+
}
|
|
124
|
+
if (!taskId) {
|
|
125
|
+
throw new Error("dag run-task requires <task-id>");
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
taskId,
|
|
129
|
+
outputPath: outputPath ? path.resolve(outputPath) : undefined,
|
|
130
|
+
strictModels,
|
|
131
|
+
noCursor,
|
|
132
|
+
execute,
|
|
133
|
+
initOnly,
|
|
134
|
+
dryRun,
|
|
135
|
+
cwd: path.resolve(cwd ?? defaultCwd ?? process.cwd()),
|
|
136
|
+
maxConcurrent,
|
|
137
|
+
runId,
|
|
138
|
+
canvasPath,
|
|
139
|
+
canvasName,
|
|
140
|
+
canvasesDir,
|
|
141
|
+
profile,
|
|
142
|
+
profileExplicit,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parseDagRunTaskProfile(value) {
|
|
146
|
+
if (value === "auto" ||
|
|
147
|
+
value === "minimal" ||
|
|
148
|
+
value === "standard" ||
|
|
149
|
+
value === "reviewed" ||
|
|
150
|
+
value === "supervised") {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`dag run-task --profile must be one of: auto, minimal, standard, reviewed, supervised; got ${value}`);
|
|
154
|
+
}
|
|
155
|
+
export function parseDagValidateArgs(args) {
|
|
156
|
+
if (args.length === 0) {
|
|
157
|
+
throw new Error("usage: dag validate --dag <path> [--strict-models] [--strict-governance]");
|
|
158
|
+
}
|
|
159
|
+
let dagPath;
|
|
160
|
+
let strictModelMatrix = false;
|
|
161
|
+
let strictGovernance = false;
|
|
162
|
+
const forbidExecutors = [];
|
|
163
|
+
let spineTaskId;
|
|
164
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
165
|
+
const arg = args[i];
|
|
166
|
+
if (arg === "--dag") {
|
|
167
|
+
dagPath = args[++i];
|
|
168
|
+
if (!dagPath) {
|
|
169
|
+
throw new Error("dag validate --dag requires a path");
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (arg.startsWith("--dag=")) {
|
|
174
|
+
dagPath = arg.slice("--dag=".length);
|
|
175
|
+
if (!dagPath) {
|
|
176
|
+
throw new Error("dag validate --dag requires a path");
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (arg === "--strict-models") {
|
|
181
|
+
strictModelMatrix = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (arg === "--strict-governance") {
|
|
185
|
+
strictGovernance = true;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (arg === "--forbid-executor") {
|
|
189
|
+
const executor = args[++i];
|
|
190
|
+
if (!executor) {
|
|
191
|
+
throw new Error("dag validate --forbid-executor requires an executor");
|
|
192
|
+
}
|
|
193
|
+
forbidExecutors.push(executor);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (arg === "--spine-task") {
|
|
197
|
+
spineTaskId = args[++i];
|
|
198
|
+
if (!spineTaskId) {
|
|
199
|
+
throw new Error("dag validate --spine-task requires a task id");
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (arg.startsWith("--spine-task=")) {
|
|
204
|
+
spineTaskId = arg.slice("--spine-task=".length);
|
|
205
|
+
if (!spineTaskId) {
|
|
206
|
+
throw new Error("dag validate --spine-task requires a task id");
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (arg.startsWith("--forbid-executor=")) {
|
|
211
|
+
const executor = arg.slice("--forbid-executor=".length);
|
|
212
|
+
if (!executor) {
|
|
213
|
+
throw new Error("dag validate --forbid-executor requires an executor");
|
|
214
|
+
}
|
|
215
|
+
forbidExecutors.push(executor);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (arg.startsWith("-")) {
|
|
219
|
+
throw new Error(`unknown dag validate flag: ${arg}`);
|
|
220
|
+
}
|
|
221
|
+
if (dagPath) {
|
|
222
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
223
|
+
}
|
|
224
|
+
dagPath = arg;
|
|
225
|
+
}
|
|
226
|
+
if (!dagPath) {
|
|
227
|
+
throw new Error("dag validate requires --dag <path>");
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
dagPath: path.resolve(dagPath),
|
|
231
|
+
strictModelMatrix,
|
|
232
|
+
strictGovernance,
|
|
233
|
+
forbidExecutors,
|
|
234
|
+
spineTaskId,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export function parseRunDagArgs(args, defaultCwd) {
|
|
238
|
+
if (args.length === 0) {
|
|
239
|
+
throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]]");
|
|
240
|
+
}
|
|
241
|
+
let dagPath;
|
|
242
|
+
let cwd;
|
|
243
|
+
let initOnly = false;
|
|
244
|
+
let dryRun = false;
|
|
245
|
+
let maxConcurrent;
|
|
246
|
+
let runId;
|
|
247
|
+
let noCursor = false;
|
|
248
|
+
let canvasPath;
|
|
249
|
+
let canvasName;
|
|
250
|
+
let canvasesDir;
|
|
251
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
252
|
+
const arg = args[i];
|
|
253
|
+
if (arg === "--dag") {
|
|
254
|
+
dagPath = args[++i];
|
|
255
|
+
}
|
|
256
|
+
else if (arg.startsWith("--dag=")) {
|
|
257
|
+
dagPath = arg.slice(6);
|
|
258
|
+
}
|
|
259
|
+
else if (arg === "--cwd" || arg === "-C") {
|
|
260
|
+
cwd = args[++i];
|
|
261
|
+
}
|
|
262
|
+
else if (arg.startsWith("--cwd=")) {
|
|
263
|
+
cwd = arg.slice(6);
|
|
264
|
+
}
|
|
265
|
+
else if (arg === "--init-only") {
|
|
266
|
+
initOnly = true;
|
|
267
|
+
}
|
|
268
|
+
else if (arg === "--dry-run") {
|
|
269
|
+
dryRun = true;
|
|
270
|
+
}
|
|
271
|
+
else if (arg === "--max-concurrent") {
|
|
272
|
+
maxConcurrent = Number(args[++i]);
|
|
273
|
+
}
|
|
274
|
+
else if (arg === "--run-id") {
|
|
275
|
+
runId = args[++i];
|
|
276
|
+
}
|
|
277
|
+
else if (arg === "--no-cursor") {
|
|
278
|
+
noCursor = true;
|
|
279
|
+
}
|
|
280
|
+
else if (arg === "--canvas-path") {
|
|
281
|
+
canvasPath = args[++i];
|
|
282
|
+
}
|
|
283
|
+
else if (arg.startsWith("--canvas-path=")) {
|
|
284
|
+
canvasPath = arg.slice(14);
|
|
285
|
+
}
|
|
286
|
+
else if (arg === "--canvas") {
|
|
287
|
+
canvasName = args[++i];
|
|
288
|
+
}
|
|
289
|
+
else if (arg.startsWith("--canvas=")) {
|
|
290
|
+
canvasName = arg.slice(9);
|
|
291
|
+
}
|
|
292
|
+
else if (arg === "--canvases-dir") {
|
|
293
|
+
canvasesDir = args[++i];
|
|
294
|
+
}
|
|
295
|
+
else if (arg.startsWith("--canvases-dir=")) {
|
|
296
|
+
canvasesDir = arg.slice(15);
|
|
297
|
+
}
|
|
298
|
+
else if (!arg.startsWith("-")) {
|
|
299
|
+
if (!dagPath)
|
|
300
|
+
dagPath = arg;
|
|
301
|
+
else
|
|
302
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
throw new Error(`unknown run-dag flag: ${arg}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!dagPath) {
|
|
309
|
+
throw new Error("run-dag requires --dag <path>");
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
dagPath: path.resolve(dagPath),
|
|
313
|
+
cwd: path.resolve(cwd ?? defaultCwd ?? process.cwd()),
|
|
314
|
+
initOnly,
|
|
315
|
+
dryRun,
|
|
316
|
+
maxConcurrent,
|
|
317
|
+
runId,
|
|
318
|
+
noCursor,
|
|
319
|
+
canvasPath,
|
|
320
|
+
canvasName,
|
|
321
|
+
canvasesDir,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
export function parseDagReportArgs(args) {
|
|
325
|
+
let runId;
|
|
326
|
+
let lifecycle = "all";
|
|
327
|
+
let json = false;
|
|
328
|
+
let markdown = false;
|
|
329
|
+
let failedOnly = false;
|
|
330
|
+
let latest = false;
|
|
331
|
+
let pausedLatest = false;
|
|
332
|
+
let action;
|
|
333
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
334
|
+
const arg = args[i];
|
|
335
|
+
if (arg === "--run-id") {
|
|
336
|
+
runId = args[++i];
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (arg.startsWith("--run-id=")) {
|
|
340
|
+
runId = arg.slice("--run-id=".length);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (arg === "--lifecycle") {
|
|
344
|
+
const value = args[++i];
|
|
345
|
+
if (!LIFECYCLE_FILTERS.has(value)) {
|
|
346
|
+
throw new Error(`invalid --lifecycle value: ${value} (expected active|paused|completed|all)`);
|
|
347
|
+
}
|
|
348
|
+
lifecycle = value;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (arg.startsWith("--lifecycle=")) {
|
|
352
|
+
const value = arg.slice("--lifecycle=".length);
|
|
353
|
+
if (!LIFECYCLE_FILTERS.has(value)) {
|
|
354
|
+
throw new Error(`invalid --lifecycle value: ${value} (expected active|paused|completed|all)`);
|
|
355
|
+
}
|
|
356
|
+
lifecycle = value;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (arg === "--json") {
|
|
360
|
+
json = true;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (arg === "--markdown") {
|
|
364
|
+
markdown = true;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (arg === "--failed-only") {
|
|
368
|
+
failedOnly = true;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (arg === "--latest") {
|
|
372
|
+
latest = true;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (arg === "--paused-latest") {
|
|
376
|
+
pausedLatest = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (arg === "--action") {
|
|
380
|
+
const value = args[++i];
|
|
381
|
+
if (!RECOVERY_ACTIONS.has(value)) {
|
|
382
|
+
throw new Error(`invalid --action value: ${value} (expected ${[...DAG_RECOVERY_ACTIONS].join("|")})`);
|
|
383
|
+
}
|
|
384
|
+
action = value;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (arg.startsWith("--action=")) {
|
|
388
|
+
const value = arg.slice("--action=".length);
|
|
389
|
+
if (!RECOVERY_ACTIONS.has(value)) {
|
|
390
|
+
throw new Error(`invalid --action value: ${value} (expected ${[...DAG_RECOVERY_ACTIONS].join("|")})`);
|
|
391
|
+
}
|
|
392
|
+
action = value;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (arg.startsWith("-")) {
|
|
396
|
+
throw new Error(`unknown dag report flag: ${arg}`);
|
|
397
|
+
}
|
|
398
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
399
|
+
}
|
|
400
|
+
if (json && markdown) {
|
|
401
|
+
throw new Error("cannot use --json and --markdown together");
|
|
402
|
+
}
|
|
403
|
+
if (pausedLatest) {
|
|
404
|
+
if (lifecycle !== "all") {
|
|
405
|
+
throw new Error("cannot combine --paused-latest with an explicit --lifecycle filter");
|
|
406
|
+
}
|
|
407
|
+
lifecycle = DAG_PAUSED_LATEST_REPORT_PRESET.lifecycle;
|
|
408
|
+
latest = DAG_PAUSED_LATEST_REPORT_PRESET.latest;
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
runId,
|
|
412
|
+
lifecycle,
|
|
413
|
+
json,
|
|
414
|
+
markdown,
|
|
415
|
+
failedOnly,
|
|
416
|
+
latest,
|
|
417
|
+
pausedLatest,
|
|
418
|
+
action,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolveAutoRoutingProfile, requiresSupervisedQualityGate, } from "../../workflows/dag/governance-profile.js";
|
|
3
|
+
import { resolveShellCommands } from "../../executors/shell-executor.js";
|
|
4
|
+
import { parseDagSpec } from "../../workflows/dag/types.js";
|
|
5
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
6
|
+
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
7
|
+
import { defaultHybridDagOutputPath, initHybridDagFromTask, } from "../../workflows/dag/init-hybrid.js";
|
|
8
|
+
import { validateDagUseCase } from "./validate-dag.js";
|
|
9
|
+
import { runDagUseCase } from "./run-dag.js";
|
|
10
|
+
const PLACEHOLDER_WRITESET_MARKER = "REPLACE/WITH";
|
|
11
|
+
function buildValidateInput(repoRoot, dagPath, parsed) {
|
|
12
|
+
return {
|
|
13
|
+
repoRoot,
|
|
14
|
+
dagPath,
|
|
15
|
+
strictModelMatrix: parsed.strictModels,
|
|
16
|
+
strictGovernance: false,
|
|
17
|
+
forbidExecutors: parsed.noCursor ? ["cursor"] : [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function shouldRunExecution(parsed) {
|
|
21
|
+
return parsed.execute || parsed.initOnly || parsed.dryRun;
|
|
22
|
+
}
|
|
23
|
+
function resolveExecutionMode(parsed) {
|
|
24
|
+
if (parsed.dryRun)
|
|
25
|
+
return "dry-run";
|
|
26
|
+
if (parsed.initOnly)
|
|
27
|
+
return "init-only";
|
|
28
|
+
return "execute";
|
|
29
|
+
}
|
|
30
|
+
function isUnsafeWriteSetEntry(entry) {
|
|
31
|
+
const normalized = entry.trim();
|
|
32
|
+
if (!normalized || normalized === "." || normalized === "./")
|
|
33
|
+
return true;
|
|
34
|
+
if (normalized === "**")
|
|
35
|
+
return true;
|
|
36
|
+
return normalized.includes(PLACEHOLDER_WRITESET_MARKER);
|
|
37
|
+
}
|
|
38
|
+
export async function assertSafeForExecution(dagPath) {
|
|
39
|
+
const raw = JSON.parse(await readFile(dagPath, "utf-8"));
|
|
40
|
+
const unsafe = [];
|
|
41
|
+
for (const task of raw.tasks ?? []) {
|
|
42
|
+
if (task.writePolicy !== "exclusive")
|
|
43
|
+
continue;
|
|
44
|
+
const writeSet = Array.isArray(task.writeSet) ? task.writeSet : [];
|
|
45
|
+
for (const entry of writeSet) {
|
|
46
|
+
if (typeof entry !== "string")
|
|
47
|
+
continue;
|
|
48
|
+
if (isUnsafeWriteSetEntry(entry)) {
|
|
49
|
+
const taskId = typeof task.id === "string" ? task.id : "unknown";
|
|
50
|
+
unsafe.push(`task=${taskId}, writeSet=${entry}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (unsafe.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
throw new Error(`refusing execution: narrow implement writeSet before --execute/init-only/dry-run: ${unsafe.join("; ")}`);
|
|
57
|
+
}
|
|
58
|
+
function buildNextSteps(taskId, outputPath, cwd) {
|
|
59
|
+
return [
|
|
60
|
+
`Review ${outputPath}`,
|
|
61
|
+
`npm run dev -- dag validate --dag ${outputPath}`,
|
|
62
|
+
`npm run dev -- run-dag --dag ${outputPath} --cwd ${cwd}`,
|
|
63
|
+
`npm run dev -- dag run-task ${taskId} --execute --cwd ${cwd}`,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function isBroadWriteSetEntryForPacket(entry) {
|
|
67
|
+
const normalized = entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
68
|
+
if (!normalized || normalized === "." || normalized === "./")
|
|
69
|
+
return true;
|
|
70
|
+
if (normalized === "**")
|
|
71
|
+
return true;
|
|
72
|
+
return normalized.includes(PLACEHOLDER_WRITESET_MARKER);
|
|
73
|
+
}
|
|
74
|
+
function resolveWritePolicyForPacket(task, spec) {
|
|
75
|
+
return task.writePolicy ?? spec.defaults?.writePolicy ?? "none";
|
|
76
|
+
}
|
|
77
|
+
function collectWriterTasksForPacket(spec) {
|
|
78
|
+
return spec.tasks.filter((task) => {
|
|
79
|
+
const writePolicy = resolveWritePolicyForPacket(task, spec);
|
|
80
|
+
return writePolicy === "exclusive" && (task.writeSet?.length ?? 0) > 0;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function findForbiddenOverlapsForPacket(task) {
|
|
84
|
+
const overlaps = [];
|
|
85
|
+
for (const writeSetEntry of task.writeSet ?? []) {
|
|
86
|
+
for (const forbiddenPath of task.forbiddenPaths ?? []) {
|
|
87
|
+
if (pathMatchesPattern(writeSetEntry, forbiddenPath) ||
|
|
88
|
+
pathMatchesPattern(forbiddenPath, writeSetEntry)) {
|
|
89
|
+
overlaps.push({ writeSetEntry, forbiddenPath });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return overlaps;
|
|
94
|
+
}
|
|
95
|
+
function isVerificationShellTask(task) {
|
|
96
|
+
if (task.executor !== "shell" || !task.shell)
|
|
97
|
+
return false;
|
|
98
|
+
if (/verify|verification/i.test(task.id))
|
|
99
|
+
return true;
|
|
100
|
+
const commands = resolveShellCommands(task.shell);
|
|
101
|
+
return commands.some((command) => /(vitest|npm run (lint|typecheck|test)|check-repo\.sh|loop-agent-standard-verify)/.test(command));
|
|
102
|
+
}
|
|
103
|
+
async function buildReviewPacket(input) {
|
|
104
|
+
const spec = parseDagSpec(JSON.parse(await readFile(input.dagPath, "utf-8")));
|
|
105
|
+
const writers = collectWriterTasksForPacket(spec).map((task) => {
|
|
106
|
+
const writeSet = task.writeSet ?? [];
|
|
107
|
+
return {
|
|
108
|
+
nodeId: task.id,
|
|
109
|
+
role: task.role,
|
|
110
|
+
writePolicy: resolveWritePolicyForPacket(task, spec),
|
|
111
|
+
writeSet,
|
|
112
|
+
broadEntries: writeSet.filter(isBroadWriteSetEntryForPacket),
|
|
113
|
+
forbiddenOverlaps: findForbiddenOverlapsForPacket(task),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
const shellTasks = spec.tasks.filter((task) => task.executor === "shell" && task.shell);
|
|
117
|
+
const shellVerification = shellTasks
|
|
118
|
+
.filter(isVerificationShellTask)
|
|
119
|
+
.map((task) => ({
|
|
120
|
+
nodeId: task.id,
|
|
121
|
+
commands: resolveShellCommands(task.shell),
|
|
122
|
+
}));
|
|
123
|
+
return {
|
|
124
|
+
profileRouting: {
|
|
125
|
+
requestedProfile: input.profileRouting.requestedProfile,
|
|
126
|
+
selectedByProfile: input.profileRouting.selectedByProfile,
|
|
127
|
+
selectedTemplate: input.profileRouting.selectedTemplate,
|
|
128
|
+
source: input.profileRouting.source,
|
|
129
|
+
...(input.profileRouting.routingReasons
|
|
130
|
+
? { routingReasons: input.profileRouting.routingReasons }
|
|
131
|
+
: {}),
|
|
132
|
+
},
|
|
133
|
+
governanceProfile: input.governanceProfile,
|
|
134
|
+
writers,
|
|
135
|
+
broadWriteSetRisk: writers.some((writer) => writer.broadEntries.length > 0),
|
|
136
|
+
forbiddenOverlapRisk: writers.some((writer) => writer.forbiddenOverlaps.length > 0),
|
|
137
|
+
shellGates: shellTasks
|
|
138
|
+
.filter((task) => task.shell?.verdictGate)
|
|
139
|
+
.map((task) => ({
|
|
140
|
+
nodeId: task.id,
|
|
141
|
+
fromNodeId: task.shell.verdictGate.fromNodeId,
|
|
142
|
+
accept: task.shell.verdictGate.accept,
|
|
143
|
+
lineMode: task.shell.verdictGate.lineMode ?? "first-non-empty",
|
|
144
|
+
label: task.shell.verdictGate.label,
|
|
145
|
+
commands: resolveShellCommands(task.shell),
|
|
146
|
+
})),
|
|
147
|
+
shellVerification,
|
|
148
|
+
decisionGates: spec.tasks.map((task) => ({
|
|
149
|
+
nodeId: task.id,
|
|
150
|
+
enabled: task.decisionGate?.enabled ?? false,
|
|
151
|
+
mode: task.decisionGate?.enabled
|
|
152
|
+
? (task.decisionGate.mode ?? "record-only")
|
|
153
|
+
: "disabled",
|
|
154
|
+
})),
|
|
155
|
+
expectedVerification: shellVerification.flatMap((entry) => entry.commands),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function templateForProfile(policy, profile) {
|
|
159
|
+
return policy.dag.profileRouting[profile];
|
|
160
|
+
}
|
|
161
|
+
async function resolveProfileRouting(repoRoot, parsed, candidateProfile) {
|
|
162
|
+
if (!parsed.profileExplicit) {
|
|
163
|
+
return {
|
|
164
|
+
requestedProfile: parsed.profile,
|
|
165
|
+
selectedByProfile: "standard",
|
|
166
|
+
selectedTemplate: "standard-dag",
|
|
167
|
+
source: "default",
|
|
168
|
+
candidateProfile,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const manifest = await loadHarnessManifest(repoRoot);
|
|
172
|
+
if (parsed.profile === "auto") {
|
|
173
|
+
const supervisedGate = requiresSupervisedQualityGate(candidateProfile);
|
|
174
|
+
const selectedByProfile = resolveAutoRoutingProfile(candidateProfile);
|
|
175
|
+
const escalatedToSupervised = supervisedGate.required && selectedByProfile === "supervised";
|
|
176
|
+
return {
|
|
177
|
+
requestedProfile: "auto",
|
|
178
|
+
selectedByProfile,
|
|
179
|
+
selectedTemplate: templateForProfile(manifest.workflowPolicy, selectedByProfile),
|
|
180
|
+
source: escalatedToSupervised
|
|
181
|
+
? "supervised-quality-gate"
|
|
182
|
+
: "workflowPolicy",
|
|
183
|
+
candidateProfile,
|
|
184
|
+
routingReasons: supervisedGate.required
|
|
185
|
+
? supervisedGate.reasons
|
|
186
|
+
: undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
requestedProfile: parsed.profile,
|
|
191
|
+
selectedByProfile: parsed.profile,
|
|
192
|
+
selectedTemplate: templateForProfile(manifest.workflowPolicy, parsed.profile),
|
|
193
|
+
source: "cli",
|
|
194
|
+
candidateProfile,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export async function generateTaskDagUseCase(input) {
|
|
198
|
+
const { repoRoot, ...parsed } = input;
|
|
199
|
+
const candidateResult = await initHybridDagFromTask(repoRoot, parsed.taskId, {
|
|
200
|
+
outputPath: parsed.outputPath,
|
|
201
|
+
template: "standard-dag",
|
|
202
|
+
});
|
|
203
|
+
const candidateValidateSummary = await validateDagUseCase(buildValidateInput(repoRoot, candidateResult.outputPath, parsed));
|
|
204
|
+
const profileRouting = await resolveProfileRouting(repoRoot, parsed, candidateValidateSummary.governanceProfile ?? {
|
|
205
|
+
profile: "standard",
|
|
206
|
+
process: [],
|
|
207
|
+
delivery: [],
|
|
208
|
+
codeChange: [],
|
|
209
|
+
reasons: ["dag run-task validate did not report governanceProfile"],
|
|
210
|
+
});
|
|
211
|
+
const initResult = profileRouting.selectedTemplate === "standard-dag"
|
|
212
|
+
? candidateResult
|
|
213
|
+
: await initHybridDagFromTask(repoRoot, parsed.taskId, {
|
|
214
|
+
outputPath: parsed.outputPath,
|
|
215
|
+
template: profileRouting.selectedTemplate,
|
|
216
|
+
});
|
|
217
|
+
const outputPath = initResult.outputPath;
|
|
218
|
+
const validateSummary = profileRouting.selectedTemplate === "standard-dag"
|
|
219
|
+
? candidateValidateSummary
|
|
220
|
+
: await validateDagUseCase(buildValidateInput(repoRoot, outputPath, parsed));
|
|
221
|
+
const governanceProfile = validateSummary.governanceProfile ?? {
|
|
222
|
+
profile: "standard",
|
|
223
|
+
process: [],
|
|
224
|
+
delivery: [],
|
|
225
|
+
codeChange: [],
|
|
226
|
+
reasons: ["dag run-task validate did not report governanceProfile"],
|
|
227
|
+
};
|
|
228
|
+
const reviewPacket = await buildReviewPacket({
|
|
229
|
+
dagPath: outputPath,
|
|
230
|
+
profileRouting,
|
|
231
|
+
governanceProfile,
|
|
232
|
+
});
|
|
233
|
+
if (!shouldRunExecution(parsed)) {
|
|
234
|
+
return {
|
|
235
|
+
mode: "generate+validate",
|
|
236
|
+
ok: true,
|
|
237
|
+
taskId: initResult.taskId,
|
|
238
|
+
outputPath,
|
|
239
|
+
defaultOutputPath: defaultHybridDagOutputPath(parsed.taskId),
|
|
240
|
+
taskCount: initResult.taskCount,
|
|
241
|
+
nodeIds: initResult.nodeIds,
|
|
242
|
+
title: validateSummary.title,
|
|
243
|
+
ranks: validateSummary.ranks,
|
|
244
|
+
governanceProfile,
|
|
245
|
+
profileRouting,
|
|
246
|
+
reviewPacket,
|
|
247
|
+
warnings: validateSummary.warnings,
|
|
248
|
+
next: buildNextSteps(parsed.taskId, outputPath, parsed.cwd),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
await assertSafeForExecution(outputPath);
|
|
252
|
+
const runSummary = await runDagUseCase({
|
|
253
|
+
repoRoot,
|
|
254
|
+
dagPath: outputPath,
|
|
255
|
+
cwd: parsed.cwd,
|
|
256
|
+
initOnly: parsed.initOnly,
|
|
257
|
+
dryRun: parsed.dryRun,
|
|
258
|
+
maxConcurrent: parsed.maxConcurrent,
|
|
259
|
+
runId: parsed.runId,
|
|
260
|
+
noCursor: parsed.noCursor,
|
|
261
|
+
canvasPath: parsed.canvasPath,
|
|
262
|
+
canvasName: parsed.canvasName,
|
|
263
|
+
canvasesDir: parsed.canvasesDir,
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
mode: resolveExecutionMode(parsed),
|
|
267
|
+
ok: true,
|
|
268
|
+
taskId: initResult.taskId,
|
|
269
|
+
outputPath,
|
|
270
|
+
taskCount: initResult.taskCount,
|
|
271
|
+
nodeIds: initResult.nodeIds,
|
|
272
|
+
title: validateSummary.title,
|
|
273
|
+
ranks: validateSummary.ranks,
|
|
274
|
+
governanceProfile,
|
|
275
|
+
profileRouting,
|
|
276
|
+
reviewPacket,
|
|
277
|
+
warnings: validateSummary.warnings,
|
|
278
|
+
run: runSummary,
|
|
279
|
+
};
|
|
280
|
+
}
|