@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
@@ -1,1232 +1,68 @@
1
- import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { DEFAULT_CURSOR_MODEL, DEFAULT_CURSOR_TIMEOUT_MS, executeCursorTask, } from "../../executors/cursor-executor.js";
4
- import { executeShellCommand, } from "../../executors/shell-executor.js";
5
- import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "../../executors/shell-write-guard.js";
6
- import { executeSingleSdkAttempt } from "../../executors/pi-sdk-executor.js";
7
- import { DEFAULT_PI_MODEL, DEFAULT_PI_PROVIDER } from "../../commands/pi-prompt.js";
8
- import { runDagReport } from "../../commands/dag-report.js";
9
- import { runDagRunTask } from "../../commands/dag-run-task.js";
10
- import { runDagValidate } from "../../commands/dag-validate.js";
11
- import { runRunDag } from "../../commands/run-dag.js";
12
- import { compileWorkflowToDag, defaultCompileManifestPath, } from "../dynamic/compile.js";
13
- import { buildWorkflowSpecForProfile, } from "../dynamic/profiles.js";
14
- import { parseWorkflowSpec } from "../dynamic/validate.js";
15
- import { workflowReportSchema } from "../dynamic/artifacts.js";
16
- import { resumeDagRun, runDag } from "../dag/runner.js";
17
- import { assertValidDagSpec } from "../dag/validate.js";
18
- import { assertDagRunTransferTargetAvailable, getDagRunDir, locateDagRun, readHumanEscalation, requirePausedDagRun, transferDagRunDir, writeDagRunState, writeHumanApprovalArtifact, } from "../dag/lifecycle.js";
19
- import { computeRunDir, persistCursorRunLog } from "../../records/one-shot-runs.js";
20
- import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
21
- import { pathMatchesPattern } from "../../shared/git-progress.js";
22
- import { repoRelativePath, toPosixPath } from "../../shared/path-refs.js";
23
- import { getLoopPaths } from "./paths.js";
1
+ import { parseDagReportArgs, parseDagRunTaskArgs, parseDagValidateArgs, parseRunDagArgs, } from "../../application/dag/args.js";
2
+ import { generateTaskDagUseCase } from "../../application/dag/generate-task-dag.js";
3
+ import { runLoopAction, } from "../../application/loop/run-action.js";
4
+ import { reportDagUseCase, serializeReportDagJson, } from "../../application/dag/report-dag.js";
5
+ import { runDagUseCase } from "../../application/dag/run-dag.js";
6
+ import { validateDagUseCase } from "../../application/dag/validate-dag.js";
7
+ import { formatDagReportHandoffMarkdown, formatDagReportMarkdown, } from "../dag/report.js";
8
+ import { loadTaskConfig } from "../../task/runtime.js";
9
+ import { appendLoopEvent } from "./events.js";
24
10
  import { appendLoopRound, readLoopRounds } from "./rounds.js";
25
11
  import { rewriteLoopContext } from "./context.js";
26
12
  import { loadLoopState } from "./state.js";
27
13
  import { drainLoopSignals, pendingLoopSignals, readLoopSignals, } from "./signals.js";
28
- import { appendLoopEvent } from "./events.js";
29
- const SUMMARY_LIMIT = 1200;
30
- const PI_REVIEW_TOOLS = ["read", "grep", "find", "ls"];
31
- function truncateSummary(text) {
32
- const normalized = text.trim();
33
- if (normalized.length <= SUMMARY_LIMIT)
34
- return normalized;
35
- return `${normalized.slice(0, SUMMARY_LIMIT)}\n[truncated]`;
36
- }
37
- function extractCommandsFromObjective(objective) {
38
- const lines = objective.split("\n");
39
- const commands = [];
40
- let inVerification = false;
41
- for (const line of lines) {
42
- if (/^##\s+/.test(line)) {
43
- inVerification = /verification|验证/i.test(line);
44
- continue;
45
- }
46
- if (!inVerification)
47
- continue;
48
- const backtick = line.match(/`([^`]+)`/);
49
- if (backtick?.[1]) {
50
- commands.push(backtick[1]);
51
- continue;
52
- }
53
- const bullet = line.match(/^[-*]\s+(.+)$/);
54
- if (bullet?.[1] && !bullet[1].includes(":")) {
55
- commands.push(bullet[1].trim());
56
- }
57
- }
58
- return commands;
59
- }
60
- export async function resolveLoopShellVerifyCommands(repoRoot, taskId, explicitCommands = []) {
61
- const commands = explicitCommands
62
- .map((command) => command.trim())
63
- .filter(Boolean);
64
- if (commands.length > 0)
65
- return commands;
66
- const objective = await readFile(getLoopPaths(repoRoot, taskId).objectivePath, "utf-8");
67
- const objectiveCommands = extractCommandsFromObjective(objective);
68
- if (objectiveCommands.length > 0)
69
- return objectiveCommands;
70
- const constraints = await readFile(path.join(getTaskPaths(repoRoot, taskId).sourceDir, "执行约束.md"), "utf-8").catch(() => "");
71
- const constraintCommands = extractCommandsFromObjective(constraints);
72
- if (constraintCommands.length > 0)
73
- return constraintCommands;
74
- throw new Error("loop shell-verify requires at least one --command or verification commands in objective.md");
75
- }
76
- export async function runLoopShellVerification(repoRoot, taskId, options = {}) {
77
- const commands = await resolveLoopShellVerifyCommands(repoRoot, taskId, options.commands);
78
- const rounds = await readLoopRounds(repoRoot, taskId);
79
- const round = rounds.length + 1;
80
- const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
81
- const timeoutMs = options.timeoutMs ?? 300_000;
82
- const results = [];
83
- for (const command of commands) {
84
- results.push(await executeShellCommand({ command, cwd, timeoutMs }));
85
- }
86
- const ok = results.every((result) => result.ok);
87
- const paths = getLoopPaths(repoRoot, taskId);
88
- await mkdir(paths.verificationDir, { recursive: true });
89
- const verificationPath = path.join(paths.verificationDir, `round-${round}.json`);
90
- const record = {
91
- schemaVersion: 1,
92
- taskId,
93
- round,
94
- action: "shell-verify",
95
- ok,
96
- commandCount: commands.length,
97
- results: results.map((result) => ({
98
- command: result.command,
99
- cwd: repoRelativePath(repoRoot, result.cwd),
100
- durationMs: result.durationMs,
101
- exitCode: result.exitCode,
102
- failureCategory: result.failureCategory,
103
- ok: result.ok,
104
- timedOut: result.timedOut,
105
- stdoutSummary: truncateSummary(result.stdout),
106
- stderrSummary: truncateSummary(result.stderr),
107
- })),
108
- recordedAt: new Date().toISOString(),
109
- };
110
- await writeFile(verificationPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
111
- const failed = results.find((result) => !result.ok);
112
- const verification = ok
113
- ? `passed: ${commands.join(" && ")}`
114
- : `failed: ${failed?.command ?? commands[0]} (${failed?.failureCategory ?? "unknown"})`;
115
- await appendLoopRound(repoRoot, taskId, {
116
- action: "shell-verify",
117
- refs: [repoRelativePath(repoRoot, verificationPath)],
118
- result: ok ? "verification passed" : "verification failed",
119
- verification,
120
- lesson: ok
121
- ? "deterministic shell verification passed"
122
- : "deterministic shell verification failed; inspect verification ref before another implementation round",
123
- next: ok
124
- ? "evaluate success criteria coverage"
125
- : "fix the failing verification command",
126
- decision: ok ? "continue" : "continue",
127
- failureCategory: ok ? undefined : failed?.failureCategory,
128
- completedCriteria: ok ? ["shell verification action executed"] : [],
129
- });
130
- await rewriteLoopContext(repoRoot, taskId);
131
- return record;
132
- }
133
- function latestVerificationSummary(rounds) {
134
- const latest = [...rounds].reverse().find((round) => round.verification.trim());
135
- return latest
136
- ? `${latest.verification}\nrefs=${latest.refs.join(", ")}`
137
- : "No verification summary recorded yet.";
138
- }
139
- export async function buildLoopPiReviewPrompt(repoRoot, taskId) {
140
- const paths = getLoopPaths(repoRoot, taskId);
141
- const [objective, context, rounds] = await Promise.all([
142
- readFile(paths.objectivePath, "utf-8"),
143
- readFile(paths.contextPath, "utf-8"),
144
- readLoopRounds(repoRoot, taskId),
145
- ]);
146
- return [
147
- "You are running as a read-only loop-agent loop Pi review action.",
148
- "Do not edit files. Do not run shell commands. Use only read, grep, find, and ls tools if tools are needed.",
149
- "Review the current loop state and return exactly one JSON object with these fields:",
150
- '{"findingSummary":"","failureCategory":"","nextHypothesis":"","recommendedAction":"implement_fix|replan|pause|done","fixScope":["path/or/component"],"rootCause":"","invariant":""}',
151
- "recommendedAction must be exactly one of: implement_fix, replan, pause, done.",
152
- "",
153
- "<objective>",
154
- objective.trim(),
155
- "</objective>",
156
- "",
157
- "<context>",
158
- context.trim(),
159
- "</context>",
160
- "",
161
- "<latest_verification>",
162
- latestVerificationSummary(rounds),
163
- "</latest_verification>",
164
- ].join("\n");
165
- }
166
- function parsePiReviewStructuredOutput(text) {
167
- const trimmed = text.trim();
168
- const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
169
- if (!jsonMatch) {
170
- throw new Error("pi-review output did not contain a JSON object");
171
- }
172
- const parsed = JSON.parse(jsonMatch[0]);
173
- for (const key of [
174
- "findingSummary",
175
- "failureCategory",
176
- "nextHypothesis",
177
- "recommendedAction",
178
- "rootCause",
179
- ]) {
180
- if (typeof parsed[key] !== "string" || parsed[key].trim().length === 0) {
181
- throw new Error(`pi-review output missing structured field: ${key}`);
182
- }
183
- }
184
- if (!Array.isArray(parsed.fixScope) || parsed.fixScope.length === 0) {
185
- throw new Error("pi-review output missing structured field: fixScope");
186
- }
187
- const findingSummary = parsed.findingSummary;
188
- const failureCategory = parsed.failureCategory;
189
- const nextHypothesis = parsed.nextHypothesis;
190
- const recommendedAction = parsed.recommendedAction;
191
- const rootCause = parsed.rootCause;
192
- if (typeof findingSummary !== "string" ||
193
- typeof failureCategory !== "string" ||
194
- typeof nextHypothesis !== "string" ||
195
- typeof recommendedAction !== "string" ||
196
- typeof rootCause !== "string") {
197
- throw new Error("pi-review output failed structured field validation");
198
- }
199
- if (!isLoopPiReviewRecommendedAction(recommendedAction)) {
200
- throw new Error(`pi-review output recommendedAction must be one of implement_fix|replan|pause|done, got: ${recommendedAction}`);
201
- }
202
- const fixScope = parsed.fixScope
203
- .filter((value) => typeof value === "string")
204
- .map((value) => value.trim())
205
- .filter(Boolean);
206
- if (fixScope.length === 0) {
207
- throw new Error("pi-review output fixScope must contain at least one non-empty string");
208
- }
209
- return {
210
- findingSummary: findingSummary.trim(),
211
- failureCategory: failureCategory.trim(),
212
- nextHypothesis: nextHypothesis.trim(),
213
- recommendedAction,
214
- fixScope,
215
- rootCause: rootCause.trim(),
216
- invariant: typeof parsed.invariant === "string" ? parsed.invariant.trim() : undefined,
217
- };
218
- }
219
- function isLoopPiReviewRecommendedAction(value) {
220
- return (value === "implement_fix" ||
221
- value === "replan" ||
222
- value === "pause" ||
223
- value === "done");
224
- }
225
- async function defaultPiReviewExecutor(input) {
226
- const startedAt = Date.now();
227
- const result = await executeSingleSdkAttempt({
228
- repoRoot: input.cwd,
229
- prompt: "You are a read-only reviewer for loop-agent loop. Return only structured JSON.",
230
- userMessage: input.prompt,
231
- attachedFiles: [],
232
- toolNames: [...input.toolNames],
233
- timeoutMs: input.timeoutMs,
234
- step: "analyze",
235
- modelConfig: {
236
- provider: input.provider,
237
- model: input.model,
238
- },
239
- });
240
- return {
241
- ok: result.ok,
242
- assistantText: result.assistantText,
243
- durationMs: Date.now() - startedAt,
244
- failureCategory: result.failureCategory,
245
- stderr: result.stderr,
246
- };
247
- }
248
- async function moveDir(fromDir, toDir) {
249
- await mkdir(path.dirname(toDir), { recursive: true });
250
- await rename(fromDir, toDir);
251
- }
252
- export async function runLoopPiReview(repoRoot, taskId, options = {}) {
253
- const provider = options.provider ?? DEFAULT_PI_PROVIDER;
254
- const model = options.model ?? DEFAULT_PI_MODEL;
255
- const timeoutMs = options.timeoutMs ?? 300_000;
256
- const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
257
- const executor = options.executor ?? defaultPiReviewExecutor;
258
- const rounds = await readLoopRounds(repoRoot, taskId);
259
- const round = rounds.length + 1;
260
- const paths = getLoopPaths(repoRoot, taskId);
261
- await mkdir(paths.reviewDir, { recursive: true });
262
- const prompt = await buildLoopPiReviewPrompt(repoRoot, taskId);
263
- const promptPath = path.join(paths.reviewDir, `round-${round}-prompt.md`);
264
- await writeFile(promptPath, prompt, "utf-8");
265
- const result = await executor({
266
- prompt,
267
- cwd,
268
- provider,
269
- model,
270
- timeoutMs,
271
- toolNames: PI_REVIEW_TOOLS,
272
- });
273
- let structured;
274
- let ok = result.ok;
275
- let failureCategory = result.failureCategory ?? "";
276
- try {
277
- structured = parsePiReviewStructuredOutput(result.assistantText);
278
- }
279
- catch (error) {
280
- ok = false;
281
- failureCategory = "invalid-structured-output";
282
- structured = {
283
- findingSummary: error instanceof Error ? error.message : "pi-review output parse failed",
284
- failureCategory,
285
- nextHypothesis: "rerun pi-review with a stricter structured-output prompt",
286
- recommendedAction: "pause",
287
- fixScope: ["unknown"],
288
- rootCause: "invalid pi-review structured output",
289
- invariant: "do not write until review output is valid",
290
- };
291
- }
292
- const harnessRunsDir = path.join(repoRoot, ".harness", "runs");
293
- const activeDir = path.join(harnessRunsDir, "active");
294
- const targetLifecycle = ok ? "completed" : "failed";
295
- const runId = `${new Date().toISOString().slice(0, 10)}-${taskId}-pi-review-r${round}`;
296
- const activeRunDir = path.join(activeDir, runId);
297
- await mkdir(activeRunDir, { recursive: true });
298
- const runMarkdown = [
299
- "# Pi Review Run",
300
- "",
301
- "| Field | Value |",
302
- "|---|---|",
303
- `| taskId | ${taskId} |`,
304
- `| status | ${ok ? "completed" : "failed"} |`,
305
- `| provider | ${provider} |`,
306
- `| model | ${model} |`,
307
- `| tools | ${PI_REVIEW_TOOLS.join(",")} |`,
308
- `| prompt | ${repoRelativePath(repoRoot, promptPath)} |`,
309
- "",
310
- "## Structured Output",
311
- "",
312
- "```json",
313
- JSON.stringify(structured, null, 2),
314
- "```",
315
- "",
316
- "## Assistant Text Summary",
317
- "",
318
- truncateSummary(result.assistantText || result.stderr || ""),
319
- "",
320
- ].join("\n");
321
- await writeFile(path.join(activeRunDir, "run.md"), runMarkdown, "utf-8");
322
- await writeFile(path.join(activeRunDir, "meta.json"), `${JSON.stringify({
323
- runId,
324
- taskId,
325
- status: ok ? "completed" : "failed",
326
- model,
327
- provider,
328
- channel: "pi-review",
329
- cwd,
330
- durationMs: result.durationMs,
331
- failureCategory,
332
- toolNames: PI_REVIEW_TOOLS,
333
- logPath: "run.md",
334
- }, null, 2)}\n`, "utf-8");
335
- const targetRunDir = path.join(harnessRunsDir, targetLifecycle, runId);
336
- await moveDir(activeRunDir, targetRunDir);
337
- const runRef = repoRelativePath(repoRoot, path.join(targetRunDir, "run.md"));
338
- const record = {
339
- schemaVersion: 1,
340
- taskId,
341
- round,
342
- action: "pi-review",
343
- ok,
344
- provider,
345
- model,
346
- toolNames: PI_REVIEW_TOOLS,
347
- promptPath: repoRelativePath(repoRoot, promptPath),
348
- runRef,
349
- structured,
350
- assistantTextSummary: truncateSummary(result.assistantText || result.stderr || ""),
351
- failureCategory,
352
- recordedAt: new Date().toISOString(),
353
- };
354
- await writeFile(path.join(paths.reviewDir, `round-${round}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf-8");
355
- await appendLoopRound(repoRoot, taskId, {
356
- action: "pi-review",
357
- refs: [runRef, repoRelativePath(repoRoot, path.join(paths.reviewDir, `round-${round}.json`))],
358
- result: ok ? "pi review completed" : "pi review failed",
359
- verification: ok
360
- ? `pi-review structured output: ${structured.findingSummary}`
361
- : `pi-review failed: ${structured.findingSummary}`,
362
- lesson: structured.findingSummary,
363
- next: structured.recommendedAction,
364
- decision: "continue",
365
- failureCategory: structured.failureCategory,
366
- completedCriteria: ok ? ["pi read-only review action executed"] : [],
367
- });
368
- await rewriteLoopContext(repoRoot, taskId);
369
- return record;
370
- }
371
- function normalizePattern(pattern) {
372
- return pattern.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/\*\*$/, "");
373
- }
374
- function patternsOverlap(allowed, forbidden) {
375
- const a = normalizePattern(allowed);
376
- const f = normalizePattern(forbidden);
377
- return (a === f ||
378
- a.startsWith(`${f}/`) ||
379
- f.startsWith(`${a}/`) ||
380
- pathMatchesPattern(a, forbidden) ||
381
- pathMatchesPattern(f, allowed));
382
- }
383
- function hasDagRound(rounds) {
384
- return rounds.some((round) => round.action === "dag");
385
- }
386
- async function readLatestPiReviewRecord(repoRoot, taskId) {
387
- const paths = getLoopPaths(repoRoot, taskId);
388
- let entries;
389
- try {
390
- entries = await readdir(paths.reviewDir);
391
- }
392
- catch {
393
- return undefined;
394
- }
395
- const roundFiles = entries
396
- .map((entry) => {
397
- const match = entry.match(/^round-(\d+)\.json$/);
398
- return match ? { entry, round: Number.parseInt(match[1], 10) } : undefined;
399
- })
400
- .filter((entry) => Boolean(entry))
401
- .sort((a, b) => b.round - a.round);
402
- for (const { entry } of roundFiles) {
403
- try {
404
- const parsed = JSON.parse(await readFile(path.join(paths.reviewDir, entry), "utf-8"));
405
- if (parsed.action === "pi-review" && parsed.structured) {
406
- return parsed;
407
- }
408
- }
409
- catch {
410
- continue;
411
- }
412
- }
413
- return undefined;
414
- }
415
- export function validateLoopCursorFixPolicy(input) {
416
- if (input.allowedPaths.length === 0) {
417
- throw new Error("loop cursor-fix requires non-empty task allowedPaths");
418
- }
419
- for (const allowed of input.allowedPaths) {
420
- for (const forbidden of input.forbiddenPaths) {
421
- if (patternsOverlap(allowed, forbidden)) {
422
- throw new Error(`loop cursor-fix allowed/forbidden paths overlap: ${allowed} vs ${forbidden}`);
423
- }
424
- }
425
- }
426
- if (input.complexity !== undefined &&
427
- input.complexity !== "small" &&
428
- !input.hasDagRound &&
429
- !input.dagFallbackReason?.trim()) {
430
- throw new Error("loop cursor-fix for medium/large tasks requires a prior loop dag round or task dagFallbackReason");
431
- }
432
- }
433
- function extractChangedPaths(result) {
434
- const raw = result.details?.changedPaths;
435
- if (!Array.isArray(raw))
436
- return undefined;
437
- return raw.filter((value) => typeof value === "string");
438
- }
439
- function filterLoopOwnedChangedPaths(changedPaths, repoRoot, runDir) {
440
- if (!runDir)
441
- return changedPaths;
442
- const runDirRelative = normalizePattern(repoRelativePath(repoRoot, runDir));
443
- return changedPaths.filter((changedPath) => {
444
- const normalized = normalizePattern(changedPath);
445
- return normalized !== runDirRelative && !normalized.startsWith(`${runDirRelative}/`);
446
- });
447
- }
448
- export async function buildLoopCursorFixPrompt(repoRoot, taskId) {
449
- const paths = getLoopPaths(repoRoot, taskId);
450
- const [objective, context] = await Promise.all([
451
- readFile(paths.objectivePath, "utf-8"),
452
- readFile(paths.contextPath, "utf-8"),
453
- ]);
454
- const config = await loadTaskConfig(repoRoot, taskId);
455
- const rounds = await readLoopRounds(repoRoot, taskId);
456
- const latestReview = await readLatestPiReviewRecord(repoRoot, taskId);
457
- validateLoopCursorFixPolicy({
458
- allowedPaths: config.allowedPaths,
459
- forbiddenPaths: config.forbiddenPaths,
460
- complexity: config.complexity,
461
- dagFallbackReason: config.dagFallbackReason ?? "",
462
- hasDagRound: hasDagRound(rounds),
463
- });
464
- return [
465
- `Task id: ${taskId}`,
466
- "Exact objective: implement the next bounded fix described by the loop context.",
467
- "",
468
- "Allowed paths:",
469
- ...config.allowedPaths.map((entry) => `- ${entry}`),
470
- "",
471
- "Forbidden paths:",
472
- ...(config.forbiddenPaths.length > 0
473
- ? config.forbiddenPaths.map((entry) => `- ${entry}`)
474
- : ["- none"]),
475
- "",
476
- "Hard constraints:",
477
- "- Preserve unrelated files and user work.",
478
- "- Do not touch forbidden paths.",
479
- "- Do not mark the loop complete; shell verification or review must follow.",
480
- "",
481
- "Expected verification:",
482
- "- Run the verification commands listed in the task constraints after the fix, or explain why they were not run.",
483
- "",
484
- "<latest_pi_review>",
485
- latestReview
486
- ? [
487
- `recommendedAction: ${latestReview.structured.recommendedAction}`,
488
- `rootCause: ${latestReview.structured.rootCause}`,
489
- `fixScope: ${latestReview.structured.fixScope.join(", ")}`,
490
- `invariant: ${latestReview.structured.invariant ?? "unknown"}`,
491
- ].join("\n")
492
- : "No structured Pi review record found.",
493
- "</latest_pi_review>",
494
- "",
495
- "<objective>",
496
- objective.trim(),
497
- "</objective>",
498
- "",
499
- "<context>",
500
- context.trim(),
501
- "</context>",
502
- ].join("\n");
503
- }
504
- async function defaultCursorFixExecutor(input) {
505
- return executeCursorTask({
506
- task: input.prompt,
507
- cwd: input.cwd,
508
- model: input.model,
509
- timeoutMs: input.timeoutMs,
510
- runDir: input.runDir,
511
- });
512
- }
513
- export async function runLoopCursorFix(repoRoot, taskId, options = {}) {
514
- const config = await loadTaskConfig(repoRoot, taskId);
515
- const rounds = await readLoopRounds(repoRoot, taskId);
516
- validateLoopCursorFixPolicy({
517
- allowedPaths: config.allowedPaths,
518
- forbiddenPaths: config.forbiddenPaths,
519
- complexity: config.complexity,
520
- dagFallbackReason: config.dagFallbackReason ?? "",
521
- hasDagRound: hasDagRound(rounds),
522
- });
523
- const model = options.model ?? config.cursorModel ?? DEFAULT_CURSOR_MODEL;
524
- const timeoutMs = options.timeoutMs ?? DEFAULT_CURSOR_TIMEOUT_MS;
525
- const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
526
- const executor = options.executor ?? defaultCursorFixExecutor;
527
- const round = rounds.length + 1;
528
- const latestReview = await readLatestPiReviewRecord(repoRoot, taskId);
529
- const prompt = await buildLoopCursorFixPrompt(repoRoot, taskId);
530
- const runDir = (await computeRunDir(repoRoot, `${taskId} cursor-fix r${round}`)) ?? undefined;
531
- const beforeStatus = await readGitStatusPorcelain(repoRoot).catch(() => "");
532
- const result = await executor({ prompt, cwd, model, timeoutMs, runDir });
533
- const explicitChangedPaths = extractChangedPaths(result);
534
- let changedPaths = explicitChangedPaths
535
- ? filterLoopOwnedChangedPaths(explicitChangedPaths, repoRoot, runDir)
536
- : undefined;
537
- const guard = explicitChangedPaths
538
- ? validateShellWriteGuard({
539
- changedPaths: changedPaths ?? [],
540
- writePolicy: "exclusive",
541
- writeSet: config.allowedPaths,
542
- forbiddenPaths: config.forbiddenPaths,
543
- })
544
- : await (async () => {
545
- const afterStatus = await readGitStatusPorcelain(repoRoot).catch(() => "");
546
- changedPaths = filterLoopOwnedChangedPaths(pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), snapshotGitStatusPorcelain(afterStatus)), repoRoot, runDir);
547
- return validateShellWriteGuard({
548
- changedPaths,
549
- writePolicy: "exclusive",
550
- writeSet: config.allowedPaths,
551
- forbiddenPaths: config.forbiddenPaths,
552
- });
553
- })();
554
- const finalResult = guard.ok
555
- ? result
556
- : {
557
- ...result,
558
- ok: false,
559
- status: "failed",
560
- failureCategory: "path-violation",
561
- stderr: [
562
- result.stderr,
563
- `loop cursor-fix path violations: ${guard.violations.join(", ")}`,
564
- ]
565
- .filter(Boolean)
566
- .join("\n"),
567
- };
568
- const persisted = await persistCursorRunLog(repoRoot, {
569
- task: prompt,
570
- model,
571
- channel: "tool",
572
- cwd,
573
- result: finalResult,
574
- taskId,
575
- runDir,
576
- });
577
- const runRef = persisted?.logPath
578
- ? repoRelativePath(repoRoot, persisted.logPath)
579
- : "";
580
- const paths = getLoopPaths(repoRoot, taskId);
581
- await mkdir(paths.reviewDir, { recursive: true });
582
- const record = {
583
- schemaVersion: 1,
584
- taskId,
585
- round,
586
- action: "cursor-fix",
587
- ok: finalResult.ok,
588
- model,
589
- runRef,
590
- allowedPaths: config.allowedPaths,
591
- forbiddenPaths: config.forbiddenPaths,
592
- changedPaths: changedPaths ?? [],
593
- writeGuardViolations: guard.violations,
594
- fixScope: latestReview?.structured.fixScope ?? [],
595
- rootCause: latestReview?.structured.rootCause ?? "",
596
- invariant: latestReview?.structured.invariant ?? "",
597
- expectedNextVerification: "loop shell-verify or required task verification commands",
598
- recordedAt: new Date().toISOString(),
599
- };
600
- const recordPath = path.join(paths.reviewDir, `round-${round}-cursor-fix.json`);
601
- await writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
602
- await appendLoopRound(repoRoot, taskId, {
603
- action: "cursor-fix",
604
- refs: [runRef, repoRelativePath(repoRoot, recordPath)].filter(Boolean),
605
- result: finalResult.ok ? "cursor fix completed" : "cursor fix failed",
606
- verification: finalResult.ok
607
- ? "cursor-fix completed; shell verification still required"
608
- : `cursor-fix failed: ${finalResult.failureCategory}`,
609
- lesson: finalResult.ok
610
- ? "cursor-fix changed only allowed paths; completion remains gated by verification"
611
- : finalResult.stderr || finalResult.stdout || "cursor-fix failed",
612
- next: "run shell verification for the cursor fix",
613
- decision: "continue",
614
- failureCategory: finalResult.ok ? undefined : finalResult.failureCategory,
615
- completedCriteria: finalResult.ok ? ["cursor bounded fix action executed"] : [],
616
- });
617
- await rewriteLoopContext(repoRoot, taskId);
618
- return record;
619
- }
620
- async function captureJsonOutput(fn) {
621
- let captured;
622
- const originalLog = console.log;
623
- console.log = (message) => {
624
- if (typeof message === "string") {
625
- captured = JSON.parse(message);
626
- return;
627
- }
628
- captured = message;
629
- };
630
- try {
631
- await fn();
632
- }
633
- finally {
634
- console.log = originalLog;
635
- }
636
- if (captured === undefined) {
637
- throw new Error("DAG command produced no JSON output");
638
- }
639
- return captured;
640
- }
14
+ import { decideNextLoopAutoAction, } from "./policy/auto-policy.js";
15
+ import { validateLoopCursorFixPolicy } from "./policy/cursor-fix-policy.js";
16
+ import { configureLoopDagDefaultRunner, } from "./actions/dag-action.js";
17
+ import { readLatestPiReviewRecord } from "./actions/pi-review.js";
18
+ import { runLoopWorkflowGateSignalAction, } from "./actions/workflow-action.js";
19
+ import { hasDagRound } from "./actions/shared.js";
20
+ export { decideNextLoopAutoAction } from "./policy/auto-policy.js";
21
+ export { validateLoopCursorFixPolicy } from "./policy/cursor-fix-policy.js";
22
+ export { resolveLoopShellVerifyCommands, runLoopShellVerification } from "./actions/shell-verify.js";
23
+ export { buildLoopPiReviewPrompt, runLoopPiReview, } from "./actions/pi-review.js";
24
+ export { buildLoopCursorFixPrompt, runLoopCursorFix } from "./actions/cursor-fix.js";
25
+ export { runLoopDagAction } from "./actions/dag-action.js";
26
+ export { runLoopWorkflowAction, runLoopWorkflowGateSignalAction, } from "./actions/workflow-action.js";
641
27
  async function defaultDagCommandRunner(repoRoot, command, args) {
642
28
  if (command === "dag-run-task") {
643
- return captureJsonOutput(() => runDagRunTask(repoRoot, args));
29
+ const parsed = parseDagRunTaskArgs(args, repoRoot);
30
+ return generateTaskDagUseCase({ repoRoot, ...parsed });
644
31
  }
645
32
  if (command === "dag-validate") {
646
- return captureJsonOutput(() => runDagValidate(repoRoot, args));
33
+ const parsed = parseDagValidateArgs(args);
34
+ const result = await validateDagUseCase({ repoRoot, ...parsed });
35
+ return { mode: "validate", ...result };
647
36
  }
648
37
  if (command === "run-dag") {
649
- return captureJsonOutput(() => runRunDag(repoRoot, args));
650
- }
651
- return captureJsonOutput(() => runDagReport(repoRoot, args));
652
- }
653
- function asRecord(value, label) {
654
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
655
- return value;
656
- }
657
- throw new Error(`${label} did not return a JSON object`);
658
- }
659
- function stringField(value, key, label) {
660
- const raw = value[key];
661
- if (typeof raw === "string" && raw.length > 0)
662
- return raw;
663
- throw new Error(`${label} missing string field: ${key}`);
664
- }
665
- function optionalStringField(value, key) {
666
- const raw = value[key];
667
- return typeof raw === "string" ? raw : "";
668
- }
669
- function relativeRef(repoRoot, value) {
670
- if (!value)
671
- return "";
672
- return path.isAbsolute(value) ? repoRelativePath(repoRoot, value) : toPosixPath(value);
673
- }
674
- function firstReportRun(report) {
675
- const root = asRecord(report, "dag report");
676
- const runs = root.runs;
677
- if (!Array.isArray(runs) || runs.length === 0) {
678
- throw new Error("dag report JSON did not contain any runs");
679
- }
680
- return asRecord(runs[0], "dag report run");
681
- }
682
- function loopDecisionForDagRun(run) {
683
- const lifecycle = optionalStringField(run, "lifecycle");
684
- const status = optionalStringField(run, "status");
685
- return lifecycle === "paused" || status === "paused" ? "pause" : "continue";
686
- }
687
- function loopResultForDagRun(run) {
688
- const lifecycle = optionalStringField(run, "lifecycle");
689
- const status = optionalStringField(run, "status");
690
- if (lifecycle === "paused" || status === "paused") {
691
- return "dag run paused";
692
- }
693
- if (status === "finished")
694
- return "dag run finished";
695
- if (status === "partial_failed" || status === "failed") {
696
- return `dag run ${status}`;
697
- }
698
- return `dag run status=${status || "unknown"}`;
699
- }
700
- function dagFailureCategory(run) {
701
- const raw = optionalStringField(run, "failureCategory");
702
- if (raw)
703
- return raw;
704
- const status = optionalStringField(run, "status");
705
- if (status === "partial_failed" || status === "failed")
706
- return status;
707
- return "";
708
- }
709
- export async function runLoopDagAction(repoRoot, taskId, options = {}) {
710
- const mode = options.mode ?? "review";
711
- const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
712
- const rounds = await readLoopRounds(repoRoot, taskId);
713
- const round = rounds.length + 1;
714
- const paths = getLoopPaths(repoRoot, taskId);
715
- const dagEvidenceDir = path.join(paths.loopDir, "dag");
716
- await mkdir(dagEvidenceDir, { recursive: true });
717
- const dagPath = options.dagPath
718
- ? path.resolve(repoRoot, options.dagPath)
719
- : path.join(dagEvidenceDir, `round-${round}.dag.json`);
720
- const runner = options.runner ??
721
- ((command, args) => defaultDagCommandRunner(repoRoot, command, args));
722
- let generated = {};
723
- if (!options.dagPath) {
724
- generated = asRecord(await runner("dag-run-task", [
725
- taskId,
726
- "--profile",
727
- "auto",
728
- "--strict-models",
729
- "--output",
730
- dagPath,
731
- "--cwd",
732
- cwd,
733
- ]), "dag run-task");
38
+ const parsed = parseRunDagArgs(args, repoRoot);
39
+ return runDagUseCase({ repoRoot, ...parsed });
734
40
  }
735
- await runner("dag-validate", [
736
- "--dag",
737
- dagPath,
738
- "--strict-models",
739
- "--strict-governance",
740
- ]);
741
- let runId = "";
742
- let runStatus = "not-run";
743
- let runLifecycle = "not-run";
744
- let runRef = "";
745
- let reportRef = "";
746
- let failureCategory = "";
747
- let decision = "continue";
748
- let result = "dag generated and strict-governance validated";
749
- let lesson = "DAG review packet is ready; inspect writeSet, gates, and expected verification before execution.";
750
- let completedCriteria = ["dag generated and strict-governance validated"];
751
- if (mode === "execute") {
752
- const runArgs = ["--dag", dagPath, "--cwd", cwd];
753
- if (options.runId)
754
- runArgs.push("--run-id", options.runId);
755
- if (options.maxConcurrent !== undefined) {
756
- runArgs.push("--max-concurrent", String(options.maxConcurrent));
757
- }
758
- if (options.noCursor)
759
- runArgs.push("--no-cursor");
760
- const runSummary = asRecord(await runner("run-dag", runArgs), "run-dag");
761
- runId = stringField(runSummary, "runId", "run-dag");
762
- const report = await runner("dag-report", [
763
- "--run-id",
764
- runId,
765
- "--json",
766
- "--lifecycle",
767
- "all",
768
- ]);
769
- const run = firstReportRun(report);
770
- runStatus = stringField(run, "status", "dag report run");
771
- runLifecycle = stringField(run, "lifecycle", "dag report run");
772
- runRef = relativeRef(repoRoot, stringField(run, "runDir", "dag report run"));
773
- failureCategory = dagFailureCategory(run);
774
- decision = loopDecisionForDagRun(run);
775
- result = loopResultForDagRun(run);
776
- lesson =
777
- decision === "pause"
778
- ? "DAG run paused for human approval; use DAG decision commands before resuming."
779
- : "DAG run facts recorded; inspect dag report and follow with shell verification or review.";
780
- completedCriteria = ["dag round action executed"];
781
- }
782
- const recordPath = path.join(dagEvidenceDir, `round-${round}.json`);
783
- reportRef = repoRelativePath(repoRoot, recordPath);
784
- const record = {
785
- schemaVersion: 1,
786
- taskId,
787
- round,
788
- action: "dag",
789
- mode,
790
- ok: mode === "review" || runStatus === "finished",
791
- dagPath: relativeRef(repoRoot, dagPath),
792
- strictValidateOk: true,
793
- runId,
794
- runStatus,
795
- runLifecycle,
796
- runRef,
797
- reportRef,
798
- reviewPacket: generated.reviewPacket,
799
- failureCategory,
800
- expectedNextVerification: decision === "pause"
801
- ? "approve or reject paused DAG run, then resume or replan"
802
- : "run shell verification or review DAG report before completion",
803
- recordedAt: new Date().toISOString(),
804
- };
805
- await writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
806
- await appendLoopRound(repoRoot, taskId, {
807
- action: "dag",
808
- refs: [runRef, record.dagPath, reportRef].filter(Boolean),
809
- result,
810
- verification: mode === "review"
811
- ? "dag generated and validated with --strict-models --strict-governance"
812
- : `dag report status=${runStatus}, lifecycle=${runLifecycle}`,
813
- lesson,
814
- next: record.expectedNextVerification,
815
- decision,
816
- failureCategory,
817
- completedCriteria,
818
- });
819
- await rewriteLoopContext(repoRoot, taskId);
820
- return record;
821
- }
822
- function summarizeWorkflowReport(raw) {
823
- if (!raw?.trim()) {
824
- return {
825
- markdown: "# Workflow Report\n\nNo final workflow report output was emitted.\n",
826
- verification: "workflow produced no final report output",
827
- result: "workflow run finished without final report output",
828
- };
829
- }
830
- try {
831
- const parsed = workflowReportSchema.parse(JSON.parse(raw));
832
- return {
833
- markdown: [
834
- "# Workflow Report",
835
- "",
836
- parsed.summary,
837
- "",
838
- `- Verified findings: ${parsed.verifiedFindings.length}`,
839
- `- Uncertain findings: ${parsed.uncertainFindings.length}`,
840
- `- Refuted findings ref: ${parsed.refutedFindingsRef ?? "none"}`,
841
- "",
842
- ].join("\n"),
843
- verification: `${parsed.verifiedFindings.length} verified, ${parsed.uncertainFindings.length} uncertain`,
844
- result: parsed.summary,
845
- };
846
- }
847
- catch {
848
- return {
849
- markdown: `# Workflow Report\n\n${raw.trim()}\n`,
850
- verification: "workflow final output was not a standard WorkflowReport",
851
- result: "workflow run produced a non-standard final report",
852
- };
853
- }
854
- }
855
- function workflowFailureCategory(nodes) {
856
- for (const node of Object.values(nodes)) {
857
- if (node.status === "ERROR")
858
- return node.failureCategory ?? "workflow-node-error";
859
- }
860
- return "";
861
- }
862
- export async function runLoopWorkflowAction(repoRoot, taskId, options = {}) {
863
- const mode = options.mode ?? "review";
864
- const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
865
- const rounds = await readLoopRounds(repoRoot, taskId);
866
- const round = rounds.length + 1;
867
- const taskPaths = getTaskPaths(repoRoot, taskId);
868
- const workflowRoot = path.join(taskPaths.taskDir, "workflows");
869
- const plannedDir = path.join(workflowRoot, "planned");
870
- const compiledDir = path.join(workflowRoot, "compiled");
871
- const reportsDir = path.join(workflowRoot, "reports");
872
- await mkdir(plannedDir, { recursive: true });
873
- await mkdir(compiledDir, { recursive: true });
874
- await mkdir(reportsDir, { recursive: true });
875
- const profile = options.profile ?? "pr-review";
876
- const workflowPath = options.workflowPath
877
- ? path.resolve(repoRoot, options.workflowPath)
878
- : path.join(plannedDir, `${profile}.workflow.json`);
879
- const workflow = options.workflowPath
880
- ? parseWorkflowSpec(JSON.parse(await readFile(workflowPath, "utf-8")))
881
- : buildWorkflowSpecForProfile({
882
- profile,
883
- taskId,
884
- base: options.base,
885
- head: options.head,
886
- changedFiles: options.changedFiles,
887
- });
888
- if (!options.workflowPath) {
889
- await writeFile(workflowPath, `${JSON.stringify(workflow, null, 2)}\n`, "utf-8");
890
- }
891
- const compiledDagPath = path.join(compiledDir, `${workflow.meta.name}.dag.json`);
892
- const compileManifestPath = defaultCompileManifestPath(compiledDagPath);
893
- const compiled = compileWorkflowToDag(workflow, { sourcePath: workflowPath });
894
- assertValidDagSpec(compiled.dag, { strictGovernance: true });
895
- await writeFile(compiledDagPath, `${JSON.stringify(compiled.dag, null, 2)}\n`, "utf-8");
896
- await writeFile(compileManifestPath, `${JSON.stringify(compiled.manifest, null, 2)}\n`, "utf-8");
897
- let runId = "";
898
- let runStatus = "not-run";
899
- let runRef = "";
900
- let failureCategory = "";
901
- let decision = "continue";
902
- let result = "workflow planned, compiled, and strict-governance validated";
903
- let verification = "workflow compiled DAG validated with strict governance";
904
- let expectedNextVerification = "inspect workflow DAG/report refs before executing or closing out";
905
- const reportPath = path.join(reportsDir, `${workflow.meta.name}.workflow-report.md`);
906
- if (mode === "execute") {
907
- const summary = await runDag(compiled.dag, {
908
- cwd,
909
- runId: options.runId,
910
- maxConcurrent: options.maxConcurrent,
911
- });
912
- runId = summary.runId;
913
- runStatus = summary.status;
914
- runRef = relativeRef(repoRoot, summary.runDir);
915
- failureCategory = workflowFailureCategory(summary.nodes);
916
- decision = runStatus === "paused" ? "pause" : "continue";
917
- expectedNextVerification =
918
- decision === "pause"
919
- ? "approve or reject paused workflow DAG run, then resume or replan"
920
- : "review workflow report and run any required shell verification";
921
- const report = summarizeWorkflowReport(summary.nodes[workflow.final.from]?.stdout);
922
- result = report.result;
923
- verification = `workflow run status=${runStatus}; ${report.verification}`;
924
- await writeFile(reportPath, report.markdown, "utf-8");
925
- }
926
- else {
927
- await writeFile(reportPath, "# Workflow Report\n\nWorkflow was compiled in review mode and not executed.\n", "utf-8");
928
- }
929
- const record = {
930
- schemaVersion: 1,
931
- taskId,
932
- round,
933
- action: "workflow",
934
- mode,
935
- profile: workflow.meta.name,
936
- ok: mode === "review" || runStatus === "finished",
937
- workflowRef: relativeRef(repoRoot, workflowPath),
938
- compiledDagRef: relativeRef(repoRoot, compiledDagPath),
939
- compileManifestRef: relativeRef(repoRoot, compileManifestPath),
940
- runId,
941
- runStatus,
942
- runRef,
943
- reportRef: relativeRef(repoRoot, reportPath),
944
- result,
945
- verification,
946
- failureCategory,
947
- expectedNextVerification,
948
- recordedAt: new Date().toISOString(),
949
- };
950
- await appendLoopRound(repoRoot, taskId, {
951
- action: "workflow",
952
- refs: [
953
- record.workflowRef,
954
- record.compiledDagRef,
955
- record.compileManifestRef,
956
- record.runRef,
957
- record.reportRef,
958
- ].filter(Boolean),
959
- result,
960
- verification,
961
- lesson: mode === "review"
962
- ? "Workflow spec is planned and compiled; inspect workflow refs before execution."
963
- : "Workflow run facts recorded by ref; keep detailed node outputs in DAG artifacts.",
964
- next: expectedNextVerification,
965
- decision,
966
- failureCategory,
967
- completedCriteria: ["workflow round action executed"],
968
- });
969
- await rewriteLoopContext(repoRoot, taskId);
970
- return record;
971
- }
972
- function dagRunIdFromRef(ref) {
973
- const normalized = ref.split(path.sep).join("/");
974
- const match = normalized.match(/\.harness\/dag-runs\/paused\/([^/]+)/);
975
- return match?.[1];
976
- }
977
- function dagRunIdFromApprovalMessage(message) {
978
- const match = message.match(/\brun[-_ ]?id\s*[:=]\s*([a-zA-Z0-9][a-zA-Z0-9_-]*)/i);
979
- return match?.[1];
980
- }
981
- async function findPausedWorkflowGateRunId(input) {
982
- const candidates = new Set();
983
- const fromMessage = dagRunIdFromApprovalMessage(input.signal.message);
984
- if (fromMessage)
985
- candidates.add(fromMessage);
986
- for (const ref of input.signal.refs) {
987
- const fromRef = dagRunIdFromRef(ref);
988
- if (fromRef)
989
- candidates.add(fromRef);
990
- }
991
- const rounds = await readLoopRounds(input.repoRoot, input.taskId);
992
- for (const round of [...rounds].reverse()) {
993
- if (round.action !== "workflow")
994
- continue;
995
- for (const ref of round.refs) {
996
- const fromRef = dagRunIdFromRef(ref);
997
- if (fromRef)
998
- candidates.add(fromRef);
999
- }
1000
- }
1001
- for (const candidate of candidates) {
1002
- const located = await locateDagRun(input.repoRoot, candidate);
1003
- if (located?.lifecycle === "paused")
1004
- return candidate;
1005
- }
1006
- return undefined;
1007
- }
1008
- async function approvePausedDagRunFromSignal(input) {
1009
- const { runDir, state } = await requirePausedDagRun(input.repoRoot, input.runId);
1010
- const nodeId = state.pausedByNodeId;
1011
- const node = state.nodes[nodeId];
1012
- if (!node) {
1013
- throw new Error(`paused node ${nodeId} not found in run state`);
1014
- }
1015
- const escalation = await readHumanEscalation(runDir, nodeId);
1016
- const options = escalation.humanEscalation.options.map((entry) => entry.id);
1017
- const selectedOption = escalation.humanEscalation.recommendedOption &&
1018
- options.includes(escalation.humanEscalation.recommendedOption)
1019
- ? escalation.humanEscalation.recommendedOption
1020
- : options[0];
1021
- if (!selectedOption) {
1022
- throw new Error(`missing human escalation options for node ${nodeId}`);
1023
- }
1024
- const activeRunDir = getDagRunDir(input.repoRoot, "active", state.runId);
1025
- await assertDagRunTransferTargetAvailable(activeRunDir);
1026
- const approvedAt = new Date().toISOString();
1027
- const artifact = {
1028
- schemaVersion: 1,
1029
- runId: state.runId,
1030
- nodeId,
1031
- decision: "approved",
1032
- selectedOption,
1033
- notes: `Approved by loop approval signal ${input.signal.id}: ${input.signal.message}`,
1034
- approvedAt,
1035
- escalationArtifactPath: node.escalationArtifactPath,
1036
- };
1037
- const approvalPath = await writeHumanApprovalArtifact({
1038
- runDir,
1039
- nodeId,
1040
- artifact,
1041
- });
1042
- node.pauseReason = undefined;
1043
- node.humanApprovalArtifactPath = approvalPath;
1044
- state.status = "running";
1045
- state.humanDecisionNodeId = nodeId;
1046
- delete state.pausedAt;
1047
- delete state.pausedByNodeId;
1048
- delete state.pauseReason;
1049
- await writeDagRunState(runDir, state);
1050
- await transferDagRunDir(runDir, activeRunDir);
1051
- return { selectedOption };
1052
- }
1053
- export async function runLoopWorkflowGateSignalAction(repoRoot, taskId, signals = []) {
1054
- const approvalSignal = pendingLoopSignals(signals).find((signal) => signal.type === "approval");
1055
- if (!approvalSignal)
1056
- return undefined;
1057
- const runId = await findPausedWorkflowGateRunId({
41
+ const parsed = parseDagReportArgs(args);
42
+ const report = await reportDagUseCase({
1058
43
  repoRoot,
1059
- taskId,
1060
- signal: approvalSignal,
1061
- });
1062
- if (!runId)
1063
- return undefined;
1064
- const rounds = await readLoopRounds(repoRoot, taskId);
1065
- const round = rounds.length + 1;
1066
- const approval = await approvePausedDagRunFromSignal({
1067
- repoRoot,
1068
- runId,
1069
- signal: approvalSignal,
1070
- });
1071
- const summary = await resumeDagRun({ cwd: repoRoot, runId });
1072
- const runRef = relativeRef(repoRoot, summary.runDir);
1073
- const failureCategory = workflowFailureCategory(summary.nodes);
1074
- const decision = summary.status === "paused" ? "pause" : "continue";
1075
- const expectedNextVerification = decision === "pause"
1076
- ? "approve or reject paused workflow DAG run, then resume or replan"
1077
- : "review resumed workflow report and run any required shell verification";
1078
- const result = `approval signal satisfied workflow human gate for DAG run ${runId}`;
1079
- const verification = `workflow gate approved with option=${approval.selectedOption}; resumed status=${summary.status}`;
1080
- const record = {
1081
- schemaVersion: 1,
1082
- taskId,
1083
- round,
1084
- action: "workflow-gate-signal",
1085
- ok: summary.status === "finished",
1086
- signalId: approvalSignal.id,
1087
- runId,
1088
- selectedOption: approval.selectedOption,
1089
- runStatus: summary.status,
1090
- runRef,
1091
- result,
1092
- verification,
1093
- failureCategory,
1094
- expectedNextVerification,
1095
- recordedAt: new Date().toISOString(),
1096
- };
1097
- await appendLoopRound(repoRoot, taskId, {
1098
- action: "workflow-gate-signal",
1099
- refs: [...approvalSignal.refs, runRef].filter(Boolean),
1100
- result,
1101
- verification,
1102
- lesson: "Loop approval signal was converted into the existing DAG approval/resume lifecycle.",
1103
- next: expectedNextVerification,
1104
- decision,
1105
- failureCategory,
1106
- completedCriteria: ["workflow human gate approval signal consumed"],
44
+ runId: parsed.runId,
45
+ lifecycle: parsed.lifecycle,
46
+ failedOnly: parsed.failedOnly,
47
+ latest: parsed.latest,
48
+ action: parsed.action,
1107
49
  });
1108
- await drainLoopSignals(repoRoot, taskId, [approvalSignal.id]);
1109
- await rewriteLoopContext(repoRoot, taskId);
1110
- return record;
1111
- }
1112
- export function decideNextLoopAutoAction(state, signals = [], options = {}) {
1113
- const pendingSignals = pendingLoopSignals(signals);
1114
- const urgent = pendingSignals.find((signal) => signal.urgent);
1115
- if (urgent) {
1116
- return {
1117
- action: "pause",
1118
- reason: `urgent ${urgent.type} signal: ${urgent.message}`,
1119
- decision: "pause",
1120
- };
50
+ if (parsed.json) {
51
+ return serializeReportDagJson(report);
1121
52
  }
1122
- const scopeChanged = pendingSignals.find((signal) => signal.type === "scope_changed");
1123
- if (scopeChanged) {
1124
- return {
1125
- action: "pause",
1126
- reason: `scope changed signal requires objective/context review: ${scopeChanged.message}`,
1127
- decision: "pause",
1128
- };
53
+ if (parsed.markdown) {
54
+ return formatDagReportHandoffMarkdown(report);
1129
55
  }
1130
- const reviewSignal = pendingSignals.find((signal) => signal.type === "review_feedback" || signal.type === "human_followup");
1131
- if (reviewSignal) {
1132
- return {
1133
- action: "pi-review",
1134
- reason: `${reviewSignal.type} signal requires read-only review: ${reviewSignal.message}`,
1135
- decision: "continue",
1136
- };
1137
- }
1138
- if (state.round >= state.maxRounds) {
1139
- return {
1140
- action: "blocked",
1141
- reason: `loop maxRounds reached (${state.round}/${state.maxRounds})`,
1142
- decision: "blocked",
1143
- };
1144
- }
1145
- if (state.failureStreak.category && state.failureStreak.count >= 2) {
1146
- return {
1147
- action: "blocked",
1148
- reason: `repeated failure category ${state.failureStreak.category} x${state.failureStreak.count}`,
1149
- decision: "blocked",
1150
- };
1151
- }
1152
- if (state.freshness.sourceStale) {
1153
- return {
1154
- action: "pause",
1155
- reason: "task source changed; rebuild or review action prompt before continuing",
1156
- decision: "pause",
1157
- };
1158
- }
1159
- if (state.lastAction === "shell-verify") {
1160
- return {
1161
- action: "pi-review",
1162
- reason: "shell verification already ran; request read-only review before next implementation decision",
1163
- decision: "continue",
1164
- };
1165
- }
1166
- if (state.lastAction === "pi-review") {
1167
- if (options.latestPiReview?.recommendedAction === "implement_fix") {
1168
- const policy = options.loopAutoWritePolicy ?? "off";
1169
- const approvalSignal = pendingSignals.find((signal) => signal.type === "approval");
1170
- const allowedByPolicy = policy === "enabled" ||
1171
- (policy === "approval-required" &&
1172
- (Boolean(options.allowCursorFix) || Boolean(approvalSignal)));
1173
- if (options.cursorFixPolicyError) {
1174
- return {
1175
- action: "pause",
1176
- reason: `cursor-fix policy guard failed: ${options.cursorFixPolicyError}`,
1177
- decision: "pause",
1178
- };
1179
- }
1180
- if (allowedByPolicy) {
1181
- return {
1182
- action: "cursor-fix",
1183
- reason: `pi-review recommended implement_fix; loopAutoWritePolicy=${policy}`,
1184
- decision: "continue",
1185
- };
1186
- }
1187
- }
1188
- return {
1189
- action: "dag",
1190
- reason: "read-only review produced next action; generate a governed DAG review packet",
1191
- decision: "continue",
1192
- };
1193
- }
1194
- const approvalSignal = pendingSignals.find((signal) => signal.type === "approval");
1195
- if (approvalSignal) {
1196
- return {
1197
- action: "dag",
1198
- reason: `approval signal received; generate next governed DAG packet: ${approvalSignal.message}`,
1199
- decision: "continue",
1200
- };
1201
- }
1202
- if (state.lastAction === "cursor-fix" || state.lastAction === "dag") {
1203
- return {
1204
- action: "shell-verify",
1205
- reason: `${state.lastAction} round must be followed by deterministic shell verification`,
1206
- decision: "continue",
1207
- };
1208
- }
1209
- return {
1210
- action: "dag",
1211
- reason: "default to DAG governed review packet for the next bounded round",
1212
- decision: "continue",
1213
- };
56
+ return formatDagReportMarkdown(report);
1214
57
  }
58
+ configureLoopDagDefaultRunner(defaultDagCommandRunner);
1215
59
  async function defaultAutoActionRunner(input) {
1216
- if (input.action === "shell-verify") {
1217
- await runLoopShellVerification(input.repoRoot, input.taskId);
1218
- return;
1219
- }
1220
- if (input.action === "pi-review") {
1221
- await runLoopPiReview(input.repoRoot, input.taskId);
1222
- return;
1223
- }
1224
- if (input.action === "cursor-fix") {
1225
- await runLoopCursorFix(input.repoRoot, input.taskId);
1226
- return;
1227
- }
1228
- if (input.action === "dag") {
1229
- await runLoopDagAction(input.repoRoot, input.taskId);
60
+ if (isLoopActionName(input.action)) {
61
+ await runLoopAction({
62
+ action: input.action,
63
+ repoRoot: input.repoRoot,
64
+ taskId: input.taskId,
65
+ });
1230
66
  return;
1231
67
  }
1232
68
  await appendLoopRound(input.repoRoot, input.taskId, {
@@ -1242,6 +78,13 @@ async function defaultAutoActionRunner(input) {
1242
78
  });
1243
79
  await rewriteLoopContext(input.repoRoot, input.taskId);
1244
80
  }
81
+ function isLoopActionName(action) {
82
+ return (action === "shell-verify" ||
83
+ action === "pi-review" ||
84
+ action === "cursor-fix" ||
85
+ action === "dag" ||
86
+ action === "workflow");
87
+ }
1245
88
  export async function runLoopAuto(repoRoot, taskId, options = {}) {
1246
89
  const maxRounds = options.maxRounds ?? 1;
1247
90
  if (!Number.isInteger(maxRounds) || maxRounds <= 0) {