@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.
Files changed (143) hide show
  1. package/AGENTS.md +62 -45
  2. package/CHANGELOG.md +60 -28
  3. package/README.md +160 -124
  4. package/bin/loop-agent.js +21 -21
  5. package/dist/adapters/index.js +3 -2
  6. package/dist/adapters/loop-agent.js +44 -2
  7. package/dist/application/dag/args.js +420 -0
  8. package/dist/application/dag/generate-task-dag.js +280 -0
  9. package/dist/application/dag/report-dag.js +14 -0
  10. package/dist/application/dag/run-dag.js +106 -0
  11. package/dist/application/dag/validate-dag.js +102 -0
  12. package/dist/application/loop/run-action.js +23 -0
  13. package/dist/cli/catalog.js +2 -237
  14. package/dist/cli/command-definitions.js +571 -0
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/program.js +65 -1
  17. package/dist/cli/router.js +13 -0
  18. package/dist/cli-governance/active-residue-check.js +38 -0
  19. package/dist/commands/dag-report.js +6 -107
  20. package/dist/commands/dag-run-task.js +8 -466
  21. package/dist/commands/dag-validate.js +7 -179
  22. package/dist/commands/examples.js +90 -0
  23. package/dist/commands/init.js +1518 -0
  24. package/dist/commands/loop.js +57 -31
  25. package/dist/commands/pi-prompt.js +2 -9
  26. package/dist/commands/run-dag.js +7 -180
  27. package/dist/executors/cursor-executor-artifacts.js +3 -4
  28. package/dist/executors/cursor-worker-client.js +13 -3
  29. package/dist/executors/dag-cursor-executor.js +2 -3
  30. package/dist/executors/dag-pi-executor.js +3 -4
  31. package/dist/executors/dag-static-executor.js +2 -5
  32. package/dist/executors/pi-defaults.js +9 -0
  33. package/dist/executors/shell-executor.js +12 -20
  34. package/dist/governance/manifest-types.js +1 -0
  35. package/dist/infrastructure/harness/active-residue-policy.js +73 -0
  36. package/dist/infrastructure/harness/artifact-store.js +72 -0
  37. package/dist/infrastructure/harness/atomic-write.js +49 -0
  38. package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
  39. package/dist/infrastructure/harness/loop-action-store.js +23 -0
  40. package/dist/infrastructure/harness/loop-store.js +41 -0
  41. package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
  42. package/dist/infrastructure/harness/task-store.js +77 -0
  43. package/dist/records/one-shot-runs.js +26 -61
  44. package/dist/records/promotion.js +3 -4
  45. package/dist/shared/artifacts-core.js +5 -5
  46. package/dist/shared/logger.js +9 -15
  47. package/dist/task/delegate.js +4 -4
  48. package/dist/task/runtime.js +5 -7
  49. package/dist/task/state.js +6 -20
  50. package/dist/workflows/dag/convergence/controller.js +277 -0
  51. package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
  52. package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
  53. package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
  54. package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
  55. package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
  56. package/dist/workflows/dag/failure-routing.js +82 -0
  57. package/dist/workflows/dag/lifecycle.js +101 -8
  58. package/dist/workflows/dag/node-execution.js +262 -0
  59. package/dist/workflows/dag/report.js +73 -1
  60. package/dist/workflows/dag/run-store.js +36 -0
  61. package/dist/workflows/dag/runner.js +82 -1341
  62. package/dist/workflows/dag/scheduler.js +84 -0
  63. package/dist/workflows/dag/upstream-artifacts.js +20 -18
  64. package/dist/workflows/loop/actions/cursor-fix.js +191 -0
  65. package/dist/workflows/loop/actions/dag-action.js +130 -0
  66. package/dist/workflows/loop/actions/pi-review.js +267 -0
  67. package/dist/workflows/loop/actions/shared.js +157 -0
  68. package/dist/workflows/loop/actions/shell-verify.js +82 -0
  69. package/dist/workflows/loop/actions/types.js +1 -0
  70. package/dist/workflows/loop/actions/workflow-action.js +255 -0
  71. package/dist/workflows/loop/actions.js +55 -1212
  72. package/dist/workflows/loop/closeout.js +5 -4
  73. package/dist/workflows/loop/context.js +2 -3
  74. package/dist/workflows/loop/events.js +3 -2
  75. package/dist/workflows/loop/policy/auto-policy.js +104 -0
  76. package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
  77. package/dist/workflows/loop/rounds.js +3 -3
  78. package/dist/workflows/loop/signals.js +4 -7
  79. package/dist/workflows/loop/state.js +11 -11
  80. package/docs/README.md +47 -44
  81. package/docs/agent-dag-recovery-playbook.md +32 -6
  82. package/docs/agent-dag-runner.md +17 -17
  83. package/docs/architecture/runtime-boundaries.md +147 -0
  84. package/docs/cursor-executor-usage.md +5 -5
  85. package/docs/decisions/README.md +2 -2
  86. package/docs/design/README.md +24 -24
  87. package/docs/development-principles.md +50 -50
  88. package/docs/dynamic-workflow-dag-engine-roadmap.md +6 -6
  89. package/docs/exec-plans/README.md +4 -4
  90. package/docs/exec-plans/active/README.md +10 -5
  91. package/docs/exec-plans/completed/README.md +9 -5
  92. package/docs/feature-workflow.md +111 -109
  93. package/docs/harness-methodology-verification.md +18 -18
  94. package/docs/loop-agent-harness.md +36 -36
  95. package/docs/production-readiness.md +96 -0
  96. package/docs/progress/README.md +2 -2
  97. package/docs/reports/README.md +4 -2
  98. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
  99. package/docs/templates/agent-dag-process-supervisor.prompt.md +2 -2
  100. package/docs/templates/agent-dag-report.schema.json +33 -2
  101. package/docs/templates/agent-dag-review-verdict.prompt.md +1 -1
  102. package/docs/templates/agent-dag.base.json +195 -195
  103. package/docs/templates/agent-dag.final-verification.json +190 -190
  104. package/docs/templates/agent-dag.schema.json +17 -17
  105. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  106. package/docs/templates/hybrid-dag.json +193 -193
  107. package/docs/templates/production-readiness-checklist.md +57 -0
  108. package/docs/templates/progress-log.md +7 -7
  109. package/docs/templates/project-start-checklist.md +8 -8
  110. package/docs/templates/qa-report.md +17 -11
  111. package/docs/templates/sprint-contract.md +19 -19
  112. package/docs/verification-matrix.md +37 -26
  113. package/examples/example-dag.json +51 -51
  114. package/examples/hybrid-loop-agent-dag.json +194 -194
  115. package/harness.json +5 -5
  116. package/package.json +62 -61
  117. package/skills/ai-engineering-context/SKILL.md +21 -21
  118. package/skills/loop-agent/SKILL.md +56 -171
  119. package/skills/loop-agent/references/README.md +6 -2
  120. package/skills/loop-agent/references/command-reference.md +107 -65
  121. package/skills/loop-agent/references/harness-policy.md +115 -115
  122. package/skills/loop-agent/references/hybrid-dag.md +30 -30
  123. package/skills/loop-agent/references/learned/README.md +13 -13
  124. package/skills/loop-agent/references/long-running-loop.md +59 -0
  125. package/skills/loop-agent/references/model-routing.md +1 -1
  126. package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
  127. package/skills/loop-agent/references/pi-prompt.md +9 -9
  128. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
  129. package/skills/loop-agent/references/post-implementation-and-patterns.md +7 -7
  130. package/skills/loop-agent/references/task-workflow.md +19 -19
  131. package/skills/loop-agent/references/verification-and-failure-handling.md +54 -0
  132. package/skills/requesting-code-review/SKILL.md +40 -40
  133. package/skills/requesting-code-review/code-reviewer.md +4 -4
  134. package/skills/systematic-debugging/CREATION-LOG.md +43 -43
  135. package/skills/systematic-debugging/SKILL.md +113 -113
  136. package/skills/systematic-debugging/condition-based-waiting.md +20 -20
  137. package/skills/systematic-debugging/defense-in-depth.md +27 -27
  138. package/skills/systematic-debugging/root-cause-tracing.md +38 -38
  139. package/skills/systematic-debugging/test-academic.md +6 -6
  140. package/skills/systematic-debugging/test-pressure-1.md +6 -6
  141. package/skills/systematic-debugging/test-pressure-2.md +2 -2
  142. package/skills/systematic-debugging/test-pressure-3.md +6 -6
  143. 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
+ }