@tea-agent/loop-agent 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +62 -45
- package/CHANGELOG.md +60 -28
- package/README.md +160 -124
- package/bin/loop-agent.js +21 -21
- 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 +106 -0
- package/dist/application/dag/validate-dag.js +102 -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 +1518 -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/failure-routing.js +82 -0
- package/dist/workflows/dag/lifecycle.js +101 -8
- package/dist/workflows/dag/node-execution.js +262 -0
- package/dist/workflows/dag/report.js +73 -1
- 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 +47 -44
- package/docs/agent-dag-recovery-playbook.md +32 -6
- package/docs/agent-dag-runner.md +17 -17
- package/docs/architecture/runtime-boundaries.md +147 -0
- package/docs/cursor-executor-usage.md +5 -5
- package/docs/decisions/README.md +2 -2
- package/docs/design/README.md +24 -24
- package/docs/development-principles.md +50 -50
- package/docs/dynamic-workflow-dag-engine-roadmap.md +6 -6
- package/docs/exec-plans/README.md +4 -4
- package/docs/exec-plans/active/README.md +10 -5
- package/docs/exec-plans/completed/README.md +9 -5
- package/docs/feature-workflow.md +111 -109
- package/docs/harness-methodology-verification.md +18 -18
- package/docs/loop-agent-harness.md +36 -36
- package/docs/production-readiness.md +96 -0
- package/docs/progress/README.md +2 -2
- package/docs/reports/README.md +4 -2
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
- package/docs/templates/agent-dag-process-supervisor.prompt.md +2 -2
- package/docs/templates/agent-dag-report.schema.json +33 -2
- package/docs/templates/agent-dag-review-verdict.prompt.md +1 -1
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +17 -17
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/production-readiness-checklist.md +57 -0
- package/docs/templates/progress-log.md +7 -7
- package/docs/templates/project-start-checklist.md +8 -8
- package/docs/templates/qa-report.md +17 -11
- package/docs/templates/sprint-contract.md +19 -19
- package/docs/verification-matrix.md +37 -26
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +5 -5
- package/package.json +62 -61
- package/skills/ai-engineering-context/SKILL.md +21 -21
- package/skills/loop-agent/SKILL.md +56 -171
- package/skills/loop-agent/references/README.md +6 -2
- package/skills/loop-agent/references/command-reference.md +107 -65
- package/skills/loop-agent/references/harness-policy.md +115 -115
- package/skills/loop-agent/references/hybrid-dag.md +30 -30
- 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/model-routing.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/pi-prompt.md +9 -9
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
- package/skills/loop-agent/references/post-implementation-and-patterns.md +7 -7
- package/skills/loop-agent/references/task-workflow.md +19 -19
- package/skills/loop-agent/references/verification-and-failure-handling.md +54 -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,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
|
+
`loop-agent dag validate --dag ${outputPath} --strict-models --strict-governance`,
|
|
62
|
+
`loop-agent run-dag --dag ${outputPath} --cwd ${cwd}`,
|
|
63
|
+
`loop-agent 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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildDagReport, serializeDagReportJson, } from "../../workflows/dag/report.js";
|
|
2
|
+
export async function reportDagUseCase(input) {
|
|
3
|
+
return buildDagReport({
|
|
4
|
+
repoRoot: input.repoRoot,
|
|
5
|
+
runId: input.runId,
|
|
6
|
+
lifecycle: input.lifecycle,
|
|
7
|
+
failedOnly: input.failedOnly,
|
|
8
|
+
latest: input.latest,
|
|
9
|
+
action: input.action,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function serializeReportDagJson(report) {
|
|
13
|
+
return serializeDagReportJson(report);
|
|
14
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createDagCanvasObserver, resolveCanvasPath, } from "../../workflows/dag/canvas-observer.js";
|
|
3
|
+
import { loadDagSpecFromFile, runDag } from "../../workflows/dag/runner.js";
|
|
4
|
+
import { assertValidDagSpec, collectForbiddenExecutorIssues, } from "../../workflows/dag/validate.js";
|
|
5
|
+
async function loadRawTaskExecutorMap(dagPath) {
|
|
6
|
+
const raw = JSON.parse(await readFile(dagPath, "utf-8"));
|
|
7
|
+
const map = new Map();
|
|
8
|
+
for (const task of raw.tasks ?? []) {
|
|
9
|
+
if (typeof task.id === "string")
|
|
10
|
+
map.set(task.id, task.executor);
|
|
11
|
+
}
|
|
12
|
+
return map;
|
|
13
|
+
}
|
|
14
|
+
function assertNoCursorTasks(spec, rawTaskExecutors) {
|
|
15
|
+
const issues = collectForbiddenExecutorIssues(spec, ["cursor"]);
|
|
16
|
+
if (issues.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
const details = issues
|
|
19
|
+
.map((issue) => {
|
|
20
|
+
const implicitDefault = rawTaskExecutors.get(issue.taskId) === undefined;
|
|
21
|
+
return `task=${issue.taskId}${implicitDefault ? " (implicit cursor default)" : ""}`;
|
|
22
|
+
})
|
|
23
|
+
.join("; ");
|
|
24
|
+
throw new Error(`--no-cursor forbids cursor executor tasks: ${details}`);
|
|
25
|
+
}
|
|
26
|
+
function getCanvasFlushErrorMessage(error) {
|
|
27
|
+
return error instanceof Error ? error.message : String(error);
|
|
28
|
+
}
|
|
29
|
+
async function flushCanvasSafely(canvas) {
|
|
30
|
+
if (!canvas)
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
await canvas.flush();
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// Canvas is a derived view; failed writes must not decide canonical DAG success.
|
|
38
|
+
return getCanvasFlushErrorMessage(error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function buildRunDagNextSteps(runId) {
|
|
42
|
+
return [
|
|
43
|
+
`loop-agent dag report --run-id ${runId} --markdown`,
|
|
44
|
+
`loop-agent dag doctor --run-id ${runId} --markdown`,
|
|
45
|
+
`loop-agent promote-run <task-id> --run-id ${runId}`,
|
|
46
|
+
`loop-agent dag closeout-draft --run-id ${runId}`,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
export async function runDagUseCase(input) {
|
|
50
|
+
const spec = await loadDagSpecFromFile(input.dagPath);
|
|
51
|
+
if (input.noCursor) {
|
|
52
|
+
const rawTaskExecutors = await loadRawTaskExecutorMap(input.dagPath);
|
|
53
|
+
assertNoCursorTasks(spec, rawTaskExecutors);
|
|
54
|
+
}
|
|
55
|
+
assertValidDagSpec(spec);
|
|
56
|
+
const resolvedCanvasPath = resolveCanvasPath({
|
|
57
|
+
canvasPath: input.canvasPath,
|
|
58
|
+
canvasName: input.canvasName,
|
|
59
|
+
canvasesDir: input.canvasesDir,
|
|
60
|
+
cwd: input.cwd,
|
|
61
|
+
});
|
|
62
|
+
const canvas = resolvedCanvasPath
|
|
63
|
+
? createDagCanvasObserver({ canvasPath: resolvedCanvasPath, spec })
|
|
64
|
+
: undefined;
|
|
65
|
+
if (input.dryRun) {
|
|
66
|
+
const summary = await runDag(spec, {
|
|
67
|
+
cwd: input.cwd,
|
|
68
|
+
dryRun: true,
|
|
69
|
+
runId: input.runId,
|
|
70
|
+
observer: canvas?.observer,
|
|
71
|
+
});
|
|
72
|
+
const canvasError = await flushCanvasSafely(canvas);
|
|
73
|
+
return {
|
|
74
|
+
mode: "dry-run",
|
|
75
|
+
title: spec.title,
|
|
76
|
+
taskCount: spec.tasks.length,
|
|
77
|
+
ranks: summary.ranks,
|
|
78
|
+
runId: summary.runId,
|
|
79
|
+
runDir: summary.runDir,
|
|
80
|
+
next: buildRunDagNextSteps(summary.runId),
|
|
81
|
+
...(resolvedCanvasPath ? { canvasPath: resolvedCanvasPath } : {}),
|
|
82
|
+
...(canvasError ? { canvasError } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const summary = await runDag(spec, {
|
|
86
|
+
cwd: input.cwd,
|
|
87
|
+
initOnly: input.initOnly,
|
|
88
|
+
dryRun: false,
|
|
89
|
+
maxConcurrent: input.maxConcurrent,
|
|
90
|
+
runId: input.runId,
|
|
91
|
+
observer: canvas?.observer,
|
|
92
|
+
});
|
|
93
|
+
const canvasError = await flushCanvasSafely(canvas);
|
|
94
|
+
if (resolvedCanvasPath) {
|
|
95
|
+
return {
|
|
96
|
+
...summary,
|
|
97
|
+
next: buildRunDagNextSteps(summary.runId),
|
|
98
|
+
canvasPath: resolvedCanvasPath,
|
|
99
|
+
...(canvasError ? { canvasError } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...summary,
|
|
104
|
+
next: buildRunDagNextSteps(summary.runId),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { loadDagSpecFromFile } from "../../workflows/dag/runner.js";
|
|
3
|
+
import { assertValidDagSpec, collectExecutorModelWarnings, collectForbiddenExecutorIssues, collectGovernanceWarnings, validateDagSpec, } from "../../workflows/dag/validate.js";
|
|
4
|
+
import { inferGovernanceProfile } from "../../workflows/dag/governance-profile.js";
|
|
5
|
+
import { topoSortToRanks } from "../../workflows/dag/topo.js";
|
|
6
|
+
import { runSpineAudit } from "../../governance/spine-audit.js";
|
|
7
|
+
function formatModelMatrixWarnings(warnings) {
|
|
8
|
+
return warnings.map((warning) => ({
|
|
9
|
+
executor: warning.executor,
|
|
10
|
+
complexity: warning.complexity,
|
|
11
|
+
expected: warning.expected,
|
|
12
|
+
actual: warning.actual,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
function formatGovernanceWarnings(warnings) {
|
|
16
|
+
return warnings.map((warning) => ({
|
|
17
|
+
type: warning.type,
|
|
18
|
+
message: warning.message,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
function ensureStrictModelMatrix(warnings) {
|
|
22
|
+
if (warnings.length === 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const details = warnings
|
|
26
|
+
.map((entry) => `executor=${entry.executor}, complexity=${entry.complexity}, expected=${entry.expected}, actual=${entry.actual}`)
|
|
27
|
+
.join("; ");
|
|
28
|
+
throw new Error(`non-canonical executorModels overrides are blocked by --strict-models: ${details}`);
|
|
29
|
+
}
|
|
30
|
+
async function loadRawTaskExecutorMap(dagPath) {
|
|
31
|
+
const raw = JSON.parse(await readFile(dagPath, "utf-8"));
|
|
32
|
+
const map = new Map();
|
|
33
|
+
for (const task of raw.tasks ?? []) {
|
|
34
|
+
if (typeof task.id === "string")
|
|
35
|
+
map.set(task.id, task.executor);
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
function ensureNoForbiddenExecutors(spec, forbidExecutors, rawTaskExecutors) {
|
|
40
|
+
const supported = new Set(["cursor", "pi", "shell", "static"]);
|
|
41
|
+
const normalized = forbidExecutors.map((executor) => {
|
|
42
|
+
if (!supported.has(executor)) {
|
|
43
|
+
throw new Error(`unsupported executor for --forbid-executor: ${executor}`);
|
|
44
|
+
}
|
|
45
|
+
return executor;
|
|
46
|
+
});
|
|
47
|
+
const issues = collectForbiddenExecutorIssues(spec, normalized);
|
|
48
|
+
if (issues.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
const details = issues
|
|
51
|
+
.map((issue) => {
|
|
52
|
+
const implicitDefault = rawTaskExecutors.get(issue.taskId) === undefined;
|
|
53
|
+
return `task=${issue.taskId}, executor=${issue.executor}${implicitDefault ? " (implicit default)" : ""}`;
|
|
54
|
+
})
|
|
55
|
+
.join("; ");
|
|
56
|
+
throw new Error(`forbidden executor present: ${details}`);
|
|
57
|
+
}
|
|
58
|
+
export async function validateDagUseCase(input) {
|
|
59
|
+
const spec = await loadDagSpecFromFile(input.dagPath);
|
|
60
|
+
const rawTaskExecutors = await loadRawTaskExecutorMap(input.dagPath);
|
|
61
|
+
ensureNoForbiddenExecutors(spec, input.forbidExecutors, rawTaskExecutors);
|
|
62
|
+
const validationIssues = validateDagSpec(spec);
|
|
63
|
+
const warnings = collectExecutorModelWarnings(spec);
|
|
64
|
+
const governanceWarnings = collectGovernanceWarnings(validationIssues);
|
|
65
|
+
if (input.strictModelMatrix) {
|
|
66
|
+
ensureStrictModelMatrix(warnings);
|
|
67
|
+
}
|
|
68
|
+
const spineAudit = input.spineTaskId
|
|
69
|
+
? await runSpineAudit(input.repoRoot, input.spineTaskId)
|
|
70
|
+
: undefined;
|
|
71
|
+
if (input.strictGovernance && spineAudit && !spineAudit.ok) {
|
|
72
|
+
throw new Error(`minimal spec spine audit failed for ${input.spineTaskId}: ${spineAudit.findings
|
|
73
|
+
.filter((finding) => finding.severity === "error")
|
|
74
|
+
.map((finding) => `${finding.type}: ${finding.message}`)
|
|
75
|
+
.join("; ")}`);
|
|
76
|
+
}
|
|
77
|
+
assertValidDagSpec(spec, {
|
|
78
|
+
issues: validationIssues,
|
|
79
|
+
strictGovernance: input.strictGovernance,
|
|
80
|
+
});
|
|
81
|
+
const { ranks } = topoSortToRanks(spec);
|
|
82
|
+
const governanceProfile = inferGovernanceProfile(spec);
|
|
83
|
+
return {
|
|
84
|
+
title: spec.title,
|
|
85
|
+
taskCount: spec.tasks.length,
|
|
86
|
+
ranks,
|
|
87
|
+
governanceProfile,
|
|
88
|
+
spineAudit: spineAudit
|
|
89
|
+
? {
|
|
90
|
+
ok: spineAudit.ok,
|
|
91
|
+
taskId: spineAudit.taskId,
|
|
92
|
+
findings: spineAudit.findings,
|
|
93
|
+
verifyCommandLabels: spineAudit.verifyCommandLabels,
|
|
94
|
+
}
|
|
95
|
+
: undefined,
|
|
96
|
+
warnings: [
|
|
97
|
+
...formatModelMatrixWarnings(warnings),
|
|
98
|
+
...formatGovernanceWarnings(governanceWarnings),
|
|
99
|
+
],
|
|
100
|
+
next: [`loop-agent run-dag --dag ${input.dagPath} --cwd ${input.repoRoot}`],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { runLoopCursorFix } from "../../workflows/loop/actions/cursor-fix.js";
|
|
2
|
+
import { runLoopDagAction } from "../../workflows/loop/actions/dag-action.js";
|
|
3
|
+
import { runLoopPiReview } from "../../workflows/loop/actions/pi-review.js";
|
|
4
|
+
import { runLoopShellVerification, } from "../../workflows/loop/actions/shell-verify.js";
|
|
5
|
+
import { runLoopWorkflowAction, runLoopWorkflowGateSignalAction, } from "../../workflows/loop/actions/workflow-action.js";
|
|
6
|
+
export async function runLoopAction(input) {
|
|
7
|
+
if (input.action === "shell-verify") {
|
|
8
|
+
return runLoopShellVerification(input.repoRoot, input.taskId, input.options);
|
|
9
|
+
}
|
|
10
|
+
if (input.action === "pi-review") {
|
|
11
|
+
return runLoopPiReview(input.repoRoot, input.taskId, input.options);
|
|
12
|
+
}
|
|
13
|
+
if (input.action === "cursor-fix") {
|
|
14
|
+
return runLoopCursorFix(input.repoRoot, input.taskId, input.options);
|
|
15
|
+
}
|
|
16
|
+
if (input.action === "dag") {
|
|
17
|
+
return runLoopDagAction(input.repoRoot, input.taskId, input.options);
|
|
18
|
+
}
|
|
19
|
+
if (input.action === "workflow") {
|
|
20
|
+
return runLoopWorkflowAction(input.repoRoot, input.taskId, input.options);
|
|
21
|
+
}
|
|
22
|
+
return runLoopWorkflowGateSignalAction(input.repoRoot, input.taskId, input.options?.signals);
|
|
23
|
+
}
|