@tea-agent/loop-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +121 -0
- package/CHANGELOG.md +37 -0
- package/README.md +144 -0
- package/bin/loop-agent.js +21 -0
- package/dist/adapters/aimax.js +91 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/loop-agent.js +98 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/catalog.js +259 -0
- package/dist/cli/help.js +55 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.js +505 -0
- package/dist/cli.js +12 -0
- package/dist/commands/closeout.js +13 -0
- package/dist/commands/coverage-audit.js +14 -0
- package/dist/commands/cursor-prompt.js +222 -0
- package/dist/commands/cursor-worker.js +43 -0
- package/dist/commands/dag-approve.js +102 -0
- package/dist/commands/dag-final-verification.js +76 -0
- package/dist/commands/dag-init-hybrid.js +56 -0
- package/dist/commands/dag-reconcile-tasks.js +51 -0
- package/dist/commands/dag-reject.js +91 -0
- package/dist/commands/dag-report.js +177 -0
- package/dist/commands/dag-resume.js +34 -0
- package/dist/commands/dag-run-task.js +470 -0
- package/dist/commands/dag-validate.js +186 -0
- package/dist/commands/dag-workflow-compile.js +91 -0
- package/dist/commands/dag-workflow-plan.js +130 -0
- package/dist/commands/dag-workflow-validate.js +66 -0
- package/dist/commands/delegate.js +132 -0
- package/dist/commands/docs-archive.js +5 -0
- package/dist/commands/docs-audit.js +5 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/goal.js +92 -0
- package/dist/commands/handoff-check.js +5 -0
- package/dist/commands/harvest.js +44 -0
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/instructions.js +195 -0
- package/dist/commands/knowledge.js +64 -0
- package/dist/commands/loop-benchmark.js +72 -0
- package/dist/commands/loop.js +241 -0
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/pi-prompt.js +181 -0
- package/dist/commands/pi-reuse-benchmark.js +153 -0
- package/dist/commands/plan-list.js +5 -0
- package/dist/commands/promote-run.js +29 -0
- package/dist/commands/reference-index.js +16 -0
- package/dist/commands/run-dag.js +184 -0
- package/dist/commands/spine.js +38 -0
- package/dist/commands/stats.js +84 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/study-init.js +192 -0
- package/dist/commands/workflow.js +259 -0
- package/dist/commands/worktree-create.js +31 -0
- package/dist/commands/worktree-list.js +5 -0
- package/dist/commands/worktree-remove.js +26 -0
- package/dist/cursor-worker-entry.js +8 -0
- package/dist/executors/config-core.js +55 -0
- package/dist/executors/config.js +2 -0
- package/dist/executors/cursor-artifacts.js +33 -0
- package/dist/executors/cursor-execution-log.js +81 -0
- package/dist/executors/cursor-executor-artifacts.js +135 -0
- package/dist/executors/cursor-executor.js +468 -0
- package/dist/executors/cursor-run.js +115 -0
- package/dist/executors/cursor-tool.js +94 -0
- package/dist/executors/cursor-worker-client.js +213 -0
- package/dist/executors/cursor-worker-protocol.js +18 -0
- package/dist/executors/cursor-worker-server.js +54 -0
- package/dist/executors/cursor-worker.js +3 -0
- package/dist/executors/cursor.js +6 -0
- package/dist/executors/dag-cursor-executor.js +88 -0
- package/dist/executors/dag-pi-executor.js +322 -0
- package/dist/executors/dag-static-executor.js +45 -0
- package/dist/executors/dag.js +4 -0
- package/dist/executors/index.js +8 -0
- package/dist/executors/model-routing.js +60 -0
- package/dist/executors/pi-event-serializer.js +43 -0
- package/dist/executors/pi-executor.js +606 -0
- package/dist/executors/pi-reuse-benchmark.js +316 -0
- package/dist/executors/pi-runtime-reuse.js +29 -0
- package/dist/executors/pi-sdk-executor.js +255 -0
- package/dist/executors/pi-sdk.js +1 -0
- package/dist/executors/pi.js +3 -0
- package/dist/executors/shell-executor.js +300 -0
- package/dist/executors/shell-presets.js +47 -0
- package/dist/executors/shell-verification.js +251 -0
- package/dist/executors/shell-write-guard.js +126 -0
- package/dist/executors/shell.js +3 -0
- package/dist/executors/static.js +1 -0
- package/dist/governance/checks.js +434 -0
- package/dist/governance/harness.js +9 -0
- package/dist/governance/index.js +3 -0
- package/dist/governance/manifest-types.js +128 -0
- package/dist/governance/manifest.js +2 -0
- package/dist/governance/path-guard.js +69 -0
- package/dist/governance/path-guards.js +2 -0
- package/dist/governance/profiles.js +3 -0
- package/dist/governance/requirement-coverage.js +425 -0
- package/dist/governance/skill-safety.js +135 -0
- package/dist/governance/spine-audit.js +152 -0
- package/dist/records/closeout.js +2 -0
- package/dist/records/harvest.js +236 -0
- package/dist/records/index.js +3 -0
- package/dist/records/one-shot-runs.js +421 -0
- package/dist/records/promotion.js +199 -0
- package/dist/shared/artifacts-core.js +88 -0
- package/dist/shared/artifacts.js +2 -0
- package/dist/shared/context-files.js +32 -0
- package/dist/shared/context.js +2 -0
- package/dist/shared/copy-dir.js +17 -0
- package/dist/shared/git-progress.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/logger.js +23 -0
- package/dist/shared/one-shot-prompt-args.js +98 -0
- package/dist/shared/path-refs.js +31 -0
- package/dist/shared/prompts.js +26 -0
- package/dist/shared/reference-context.js +238 -0
- package/dist/shared/timeout-policy.js +19 -0
- package/dist/shared/timeout.js +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/task/config-types.js +97 -0
- package/dist/task/config.js +2 -0
- package/dist/task/delegate.js +220 -0
- package/dist/task/goal-audit.js +51 -0
- package/dist/task/goal-policy.js +8 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/ids.js +1 -0
- package/dist/task/index.js +9 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/read-model.js +149 -0
- package/dist/task/runtime.js +699 -0
- package/dist/task/source-state.js +1 -0
- package/dist/task/state.js +55 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/task/workflow-state-types.js +92 -0
- package/dist/task/worktree-cleanup.js +140 -0
- package/dist/task/worktree.js +171 -0
- package/dist/workflows/dag/authoring.js +8 -0
- package/dist/workflows/dag/authority-surface.js +138 -0
- package/dist/workflows/dag/canvas-observer.js +474 -0
- package/dist/workflows/dag/decision-envelope.js +502 -0
- package/dist/workflows/dag/decision-evidence.js +153 -0
- package/dist/workflows/dag/decision-gates.js +1 -0
- package/dist/workflows/dag/executor-registry.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/failure-category.js +111 -0
- package/dist/workflows/dag/final-verification.js +180 -0
- package/dist/workflows/dag/governance-constants.js +5 -0
- package/dist/workflows/dag/governance-profile.js +405 -0
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +855 -0
- package/dist/workflows/dag/knowledge-curator.js +162 -0
- package/dist/workflows/dag/lifecycle.js +484 -0
- package/dist/workflows/dag/prompt-source.js +88 -0
- package/dist/workflows/dag/prompt.js +130 -0
- package/dist/workflows/dag/reconcile-tasks.js +404 -0
- package/dist/workflows/dag/recovery-recommendation.js +226 -0
- package/dist/workflows/dag/repair-artifact.js +136 -0
- package/dist/workflows/dag/report.js +1019 -0
- package/dist/workflows/dag/runner.js +1677 -0
- package/dist/workflows/dag/runtime.js +5 -0
- package/dist/workflows/dag/skill-instructions.js +471 -0
- package/dist/workflows/dag/skills.js +41 -0
- package/dist/workflows/dag/spec.js +3 -0
- package/dist/workflows/dag/topo.js +30 -0
- package/dist/workflows/dag/types.js +275 -0
- package/dist/workflows/dag/upstream-artifacts.js +95 -0
- package/dist/workflows/dag/validate.js +527 -0
- package/dist/workflows/dynamic/artifacts.js +65 -0
- package/dist/workflows/dynamic/compile.js +360 -0
- package/dist/workflows/dynamic/compileTypes.js +1 -0
- package/dist/workflows/dynamic/errors.js +5 -0
- package/dist/workflows/dynamic/index.js +7 -0
- package/dist/workflows/dynamic/profiles.js +156 -0
- package/dist/workflows/dynamic/spec.js +114 -0
- package/dist/workflows/dynamic/validate.js +275 -0
- package/dist/workflows/loop/actions.js +1334 -0
- package/dist/workflows/loop/benchmark.js +510 -0
- package/dist/workflows/loop/closeout.js +134 -0
- package/dist/workflows/loop/context.js +48 -0
- package/dist/workflows/loop/events.js +25 -0
- package/dist/workflows/loop/hash.js +32 -0
- package/dist/workflows/loop/index.js +8 -0
- package/dist/workflows/loop/paths.js +17 -0
- package/dist/workflows/loop/rounds.js +81 -0
- package/dist/workflows/loop/signals.js +55 -0
- package/dist/workflows/loop/state.js +116 -0
- package/dist/workflows/loop/templates.js +54 -0
- package/dist/workflows/loop/types.js +28 -0
- package/docs/README.md +62 -0
- package/docs/agent-dag-recovery-playbook.md +158 -0
- package/docs/agent-dag-runner.md +40 -0
- package/docs/cursor-executor-usage.md +25 -0
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +36 -0
- package/docs/development-principles.md +71 -0
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
- package/docs/exec-plans/README.md +6 -0
- package/docs/exec-plans/active/README.md +5 -0
- package/docs/exec-plans/completed/README.md +5 -0
- package/docs/feature-workflow.md +184 -0
- package/docs/harness-methodology-debugging.md +153 -0
- package/docs/harness-methodology-tdd.md +130 -0
- package/docs/harness-methodology-verification.md +27 -0
- package/docs/loop-agent-harness.md +42 -0
- package/docs/progress/README.md +3 -0
- package/docs/reports/README.md +3 -0
- package/docs/templates/adr.md +60 -0
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
- package/docs/templates/agent-dag-report.schema.json +423 -0
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
- package/docs/templates/agent-dag.base.json +195 -0
- package/docs/templates/agent-dag.final-verification.json +190 -0
- package/docs/templates/agent-dag.schema.json +316 -0
- package/docs/templates/agent-dag.supervised-implementation.json +500 -0
- package/docs/templates/exec-plan.md +64 -0
- package/docs/templates/feature-spec.md +53 -0
- package/docs/templates/hybrid-dag.json +193 -0
- package/docs/templates/progress-log.md +17 -0
- package/docs/templates/project-start-checklist.md +9 -0
- package/docs/templates/qa-report.md +42 -0
- package/docs/templates/sprint-contract.md +29 -0
- package/docs/verification-matrix.md +30 -0
- package/examples/decision-gate-agent-dag.json +123 -0
- package/examples/example-dag.json +51 -0
- package/examples/hybrid-loop-agent-dag.json +194 -0
- package/harness.json +92 -0
- package/package.json +61 -0
- package/skills/ai-engineering-context/SKILL.md +48 -0
- package/skills/loop-agent/SKILL.md +260 -0
- package/skills/loop-agent/references/README.md +63 -0
- package/skills/loop-agent/references/command-reference.md +315 -0
- package/skills/loop-agent/references/harness-policy.md +258 -0
- package/skills/loop-agent/references/hybrid-dag.md +216 -0
- package/skills/loop-agent/references/learned/README.md +21 -0
- package/skills/loop-agent/references/model-routing.md +36 -0
- package/skills/loop-agent/references/multi-worktree.md +54 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -0
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
- package/skills/loop-agent/references/pi-prompt.md +23 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
- package/skills/loop-agent/references/task-workflow.md +84 -0
- package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/verification-before-completion/SKILL.md +154 -0
|
@@ -0,0 +1,1334 @@
|
|
|
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";
|
|
24
|
+
import { appendLoopRound, readLoopRounds } from "./rounds.js";
|
|
25
|
+
import { rewriteLoopContext } from "./context.js";
|
|
26
|
+
import { loadLoopState } from "./state.js";
|
|
27
|
+
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
|
+
}
|
|
641
|
+
async function defaultDagCommandRunner(repoRoot, command, args) {
|
|
642
|
+
if (command === "dag-run-task") {
|
|
643
|
+
return captureJsonOutput(() => runDagRunTask(repoRoot, args));
|
|
644
|
+
}
|
|
645
|
+
if (command === "dag-validate") {
|
|
646
|
+
return captureJsonOutput(() => runDagValidate(repoRoot, args));
|
|
647
|
+
}
|
|
648
|
+
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");
|
|
734
|
+
}
|
|
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({
|
|
1058
|
+
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"],
|
|
1107
|
+
});
|
|
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
|
+
};
|
|
1121
|
+
}
|
|
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
|
+
};
|
|
1129
|
+
}
|
|
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
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
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);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
await appendLoopRound(input.repoRoot, input.taskId, {
|
|
1233
|
+
action: `auto-${input.action}`,
|
|
1234
|
+
result: input.reason,
|
|
1235
|
+
verification: "deterministic auto policy decision",
|
|
1236
|
+
lesson: input.reason,
|
|
1237
|
+
next: input.action === "blocked"
|
|
1238
|
+
? "manual intervention required before auto loop can continue"
|
|
1239
|
+
: "review source changes before continuing",
|
|
1240
|
+
decision: input.decision,
|
|
1241
|
+
failureCategory: input.action === "blocked" ? "auto-policy-blocked" : undefined,
|
|
1242
|
+
});
|
|
1243
|
+
await rewriteLoopContext(input.repoRoot, input.taskId);
|
|
1244
|
+
}
|
|
1245
|
+
export async function runLoopAuto(repoRoot, taskId, options = {}) {
|
|
1246
|
+
const maxRounds = options.maxRounds ?? 1;
|
|
1247
|
+
if (!Number.isInteger(maxRounds) || maxRounds <= 0) {
|
|
1248
|
+
throw new Error("loop auto --max-rounds must be a positive integer");
|
|
1249
|
+
}
|
|
1250
|
+
const runner = options.runner ?? defaultAutoActionRunner;
|
|
1251
|
+
const actions = [];
|
|
1252
|
+
for (let index = 0; index < maxRounds; index += 1) {
|
|
1253
|
+
const state = await loadLoopState(repoRoot, taskId);
|
|
1254
|
+
const signals = await readLoopSignals(repoRoot, taskId);
|
|
1255
|
+
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
1256
|
+
const latestPiReview = await readLatestPiReviewRecord(repoRoot, taskId);
|
|
1257
|
+
const pendingSignalIds = pendingLoopSignals(signals).map((signal) => signal.id);
|
|
1258
|
+
const workflowGateSignalRecord = await runLoopWorkflowGateSignalAction(repoRoot, taskId, signals);
|
|
1259
|
+
if (workflowGateSignalRecord) {
|
|
1260
|
+
const decision = {
|
|
1261
|
+
action: "workflow",
|
|
1262
|
+
reason: `approval signal satisfied paused workflow gate for run ${workflowGateSignalRecord.runId}`,
|
|
1263
|
+
decision: workflowGateSignalRecord.runStatus === "paused" ? "pause" : "continue",
|
|
1264
|
+
};
|
|
1265
|
+
actions.push(decision);
|
|
1266
|
+
await appendLoopEvent(repoRoot, taskId, {
|
|
1267
|
+
type: "action_start",
|
|
1268
|
+
round: state.round + 1,
|
|
1269
|
+
message: `auto selected ${decision.action}: ${decision.reason}`,
|
|
1270
|
+
});
|
|
1271
|
+
const updated = await loadLoopState(repoRoot, taskId);
|
|
1272
|
+
if (updated.lastDecision !== "continue" || updated.status !== "running") {
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const hasApprovalSignal = pendingLoopSignals(signals).some((signal) => signal.type === "approval");
|
|
1278
|
+
const policyAllowsCursorFix = taskConfig.loopAutoWritePolicy === "enabled" ||
|
|
1279
|
+
(taskConfig.loopAutoWritePolicy === "approval-required" &&
|
|
1280
|
+
(Boolean(options.allowCursorFix) || hasApprovalSignal));
|
|
1281
|
+
let cursorFixPolicyError;
|
|
1282
|
+
if (policyAllowsCursorFix &&
|
|
1283
|
+
state.lastAction === "pi-review" &&
|
|
1284
|
+
latestPiReview?.structured.recommendedAction === "implement_fix") {
|
|
1285
|
+
try {
|
|
1286
|
+
const rounds = await readLoopRounds(repoRoot, taskId);
|
|
1287
|
+
validateLoopCursorFixPolicy({
|
|
1288
|
+
allowedPaths: taskConfig.allowedPaths,
|
|
1289
|
+
forbiddenPaths: taskConfig.forbiddenPaths,
|
|
1290
|
+
complexity: taskConfig.complexity,
|
|
1291
|
+
dagFallbackReason: taskConfig.dagFallbackReason ?? "",
|
|
1292
|
+
hasDagRound: hasDagRound(rounds),
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
catch (error) {
|
|
1296
|
+
cursorFixPolicyError = error instanceof Error ? error.message : String(error);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const next = decideNextLoopAutoAction(state, signals, {
|
|
1300
|
+
loopAutoWritePolicy: taskConfig.loopAutoWritePolicy,
|
|
1301
|
+
allowCursorFix: options.allowCursorFix,
|
|
1302
|
+
latestPiReview: latestPiReview?.structured,
|
|
1303
|
+
cursorFixPolicyError,
|
|
1304
|
+
});
|
|
1305
|
+
actions.push(next);
|
|
1306
|
+
await appendLoopEvent(repoRoot, taskId, {
|
|
1307
|
+
type: "action_start",
|
|
1308
|
+
round: state.round + 1,
|
|
1309
|
+
message: `auto selected ${next.action}: ${next.reason}`,
|
|
1310
|
+
});
|
|
1311
|
+
await runner({ ...next, repoRoot, taskId });
|
|
1312
|
+
if (pendingSignalIds.length > 0) {
|
|
1313
|
+
await drainLoopSignals(repoRoot, taskId, pendingSignalIds);
|
|
1314
|
+
}
|
|
1315
|
+
const updated = await loadLoopState(repoRoot, taskId);
|
|
1316
|
+
if (updated.lastDecision !== "continue" || updated.status !== "running") {
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
const finalState = await loadLoopState(repoRoot, taskId);
|
|
1321
|
+
await appendLoopEvent(repoRoot, taskId, {
|
|
1322
|
+
type: "loop_finish",
|
|
1323
|
+
round: finalState.round,
|
|
1324
|
+
message: `auto loop stopped with status ${finalState.status}`,
|
|
1325
|
+
});
|
|
1326
|
+
return {
|
|
1327
|
+
schemaVersion: 1,
|
|
1328
|
+
taskId,
|
|
1329
|
+
actions,
|
|
1330
|
+
roundsRun: actions.length,
|
|
1331
|
+
finalStatus: finalState.status,
|
|
1332
|
+
recordedAt: new Date().toISOString(),
|
|
1333
|
+
};
|
|
1334
|
+
}
|