@tea-agent/loop-agent 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/README.md +127 -92
- package/dist/adapters/index.js +3 -2
- package/dist/adapters/loop-agent.js +44 -2
- package/dist/application/dag/args.js +420 -0
- package/dist/application/dag/generate-task-dag.js +280 -0
- package/dist/application/dag/report-dag.js +14 -0
- package/dist/application/dag/run-dag.js +93 -0
- package/dist/application/dag/validate-dag.js +101 -0
- package/dist/application/loop/run-action.js +23 -0
- package/dist/cli/catalog.js +2 -237
- package/dist/cli/command-definitions.js +571 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/program.js +65 -1
- package/dist/cli/router.js +13 -0
- package/dist/cli-governance/active-residue-check.js +38 -0
- package/dist/commands/dag-report.js +6 -107
- package/dist/commands/dag-run-task.js +8 -466
- package/dist/commands/dag-validate.js +7 -179
- package/dist/commands/examples.js +90 -0
- package/dist/commands/init.js +1495 -0
- package/dist/commands/loop.js +57 -31
- package/dist/commands/pi-prompt.js +2 -9
- package/dist/commands/run-dag.js +7 -180
- package/dist/executors/cursor-executor-artifacts.js +3 -4
- package/dist/executors/cursor-worker-client.js +13 -3
- package/dist/executors/dag-cursor-executor.js +2 -3
- package/dist/executors/dag-pi-executor.js +3 -4
- package/dist/executors/dag-static-executor.js +2 -5
- package/dist/executors/pi-defaults.js +9 -0
- package/dist/executors/shell-executor.js +12 -20
- package/dist/governance/manifest-types.js +1 -0
- package/dist/infrastructure/harness/active-residue-policy.js +73 -0
- package/dist/infrastructure/harness/artifact-store.js +72 -0
- package/dist/infrastructure/harness/atomic-write.js +49 -0
- package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
- package/dist/infrastructure/harness/loop-action-store.js +23 -0
- package/dist/infrastructure/harness/loop-store.js +41 -0
- package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
- package/dist/infrastructure/harness/task-store.js +77 -0
- package/dist/records/one-shot-runs.js +26 -61
- package/dist/records/promotion.js +3 -4
- package/dist/shared/artifacts-core.js +5 -5
- package/dist/shared/logger.js +9 -15
- package/dist/task/delegate.js +4 -4
- package/dist/task/runtime.js +5 -7
- package/dist/task/state.js +6 -20
- package/dist/workflows/dag/convergence/controller.js +277 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
- package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
- package/dist/workflows/dag/lifecycle.js +6 -5
- package/dist/workflows/dag/node-execution.js +262 -0
- package/dist/workflows/dag/run-store.js +36 -0
- package/dist/workflows/dag/runner.js +82 -1341
- package/dist/workflows/dag/scheduler.js +84 -0
- package/dist/workflows/dag/upstream-artifacts.js +20 -18
- package/dist/workflows/loop/actions/cursor-fix.js +191 -0
- package/dist/workflows/loop/actions/dag-action.js +130 -0
- package/dist/workflows/loop/actions/pi-review.js +267 -0
- package/dist/workflows/loop/actions/shared.js +157 -0
- package/dist/workflows/loop/actions/shell-verify.js +82 -0
- package/dist/workflows/loop/actions/types.js +1 -0
- package/dist/workflows/loop/actions/workflow-action.js +255 -0
- package/dist/workflows/loop/actions.js +55 -1212
- package/dist/workflows/loop/closeout.js +5 -4
- package/dist/workflows/loop/context.js +2 -3
- package/dist/workflows/loop/events.js +3 -2
- package/dist/workflows/loop/policy/auto-policy.js +104 -0
- package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
- package/dist/workflows/loop/rounds.js +3 -3
- package/dist/workflows/loop/signals.js +4 -7
- package/dist/workflows/loop/state.js +11 -11
- package/docs/README.md +3 -2
- package/docs/architecture/runtime-boundaries.md +147 -0
- package/docs/exec-plans/active/README.md +4 -0
- package/docs/exec-plans/completed/README.md +6 -2
- package/package.json +2 -1
- package/skills/ai-engineering-context/SKILL.md +21 -21
- package/skills/loop-agent/SKILL.md +73 -188
- package/skills/loop-agent/references/README.md +6 -2
- package/skills/loop-agent/references/harness-policy.md +113 -113
- package/skills/loop-agent/references/learned/README.md +13 -13
- package/skills/loop-agent/references/long-running-loop.md +59 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
- package/skills/loop-agent/references/verification-and-failure-handling.md +18 -0
- package/skills/requesting-code-review/SKILL.md +40 -40
- package/skills/requesting-code-review/code-reviewer.md +4 -4
- package/skills/systematic-debugging/CREATION-LOG.md +43 -43
- package/skills/systematic-debugging/SKILL.md +113 -113
- package/skills/systematic-debugging/condition-based-waiting.md +20 -20
- package/skills/systematic-debugging/defense-in-depth.md +27 -27
- package/skills/systematic-debugging/root-cause-tracing.md +38 -38
- package/skills/systematic-debugging/test-academic.md +6 -6
- package/skills/systematic-debugging/test-pressure-1.md +6 -6
- package/skills/systematic-debugging/test-pressure-2.md +2 -2
- package/skills/systematic-debugging/test-pressure-3.md +6 -6
- package/skills/verification-before-completion/SKILL.md +37 -37
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isPauseOnHumanDecisionGate } from "./decision-envelope.js";
|
|
2
|
+
export function isConditionSkippedReason(reason) {
|
|
3
|
+
return Boolean(reason?.startsWith("condition "));
|
|
4
|
+
}
|
|
5
|
+
function shouldSkipNode(node, task, nodes) {
|
|
6
|
+
if (!node || node.status !== "PENDING")
|
|
7
|
+
return false;
|
|
8
|
+
return task.depends_on.some((depId) => {
|
|
9
|
+
const dep = nodes[depId];
|
|
10
|
+
return !dep || dep.status === "ERROR" || dep.status === "SKIPPED";
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function conditionSkippedByAncestor(task, nodes) {
|
|
14
|
+
return task.depends_on.some((depId) => isConditionSkippedReason(nodes[depId]?.skippedReason));
|
|
15
|
+
}
|
|
16
|
+
async function mapConcurrent(items, limit, fn) {
|
|
17
|
+
const executing = new Set();
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
const job = fn(item).finally(() => executing.delete(job));
|
|
20
|
+
executing.add(job);
|
|
21
|
+
if (executing.size >= limit) {
|
|
22
|
+
await Promise.race(executing);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await Promise.all(executing);
|
|
26
|
+
}
|
|
27
|
+
export async function executeDagRanksOnce(input) {
|
|
28
|
+
let pausedByNodeId;
|
|
29
|
+
for (const rank of input.ranks) {
|
|
30
|
+
const runnable = rank.filter((id) => {
|
|
31
|
+
const node = input.state.nodes[id];
|
|
32
|
+
const task = input.tasksById.get(id);
|
|
33
|
+
if (!node || node.status !== "PENDING")
|
|
34
|
+
return false;
|
|
35
|
+
return !shouldSkipNode(node, task, input.state.nodes);
|
|
36
|
+
});
|
|
37
|
+
const blocked = rank.filter((id) => {
|
|
38
|
+
const node = input.state.nodes[id];
|
|
39
|
+
const task = input.tasksById.get(id);
|
|
40
|
+
return (node?.status === "PENDING" &&
|
|
41
|
+
shouldSkipNode(node, task, input.state.nodes));
|
|
42
|
+
});
|
|
43
|
+
for (const id of blocked) {
|
|
44
|
+
const node = input.state.nodes[id];
|
|
45
|
+
node.status = "SKIPPED";
|
|
46
|
+
node.skippedReason = conditionSkippedByAncestor(input.tasksById.get(id), input.state.nodes)
|
|
47
|
+
? "condition ancestor branch not selected"
|
|
48
|
+
: "upstream dependency failed or was skipped";
|
|
49
|
+
}
|
|
50
|
+
if (blocked.length > 0) {
|
|
51
|
+
await input.persistState();
|
|
52
|
+
}
|
|
53
|
+
const pauseGateRunnable = runnable.filter((id) => {
|
|
54
|
+
const task = input.tasksById.get(id);
|
|
55
|
+
return isPauseOnHumanDecisionGate(task);
|
|
56
|
+
});
|
|
57
|
+
const regularRunnable = runnable.filter((id) => !pauseGateRunnable.includes(id));
|
|
58
|
+
const rankCursorNodeIds = runnable.filter((id) => input.tasksById.get(id)?.executor === "cursor");
|
|
59
|
+
const executeNode = input.createExecuteNodeForRank(rankCursorNodeIds);
|
|
60
|
+
for (const nodeId of pauseGateRunnable) {
|
|
61
|
+
await input.executeScheduledNode(nodeId, executeNode, (id, pausedAt, pauseReason) => {
|
|
62
|
+
pausedByNodeId = id;
|
|
63
|
+
input.state.pausedAt = pausedAt;
|
|
64
|
+
input.state.pausedByNodeId = id;
|
|
65
|
+
input.state.pauseReason = pauseReason;
|
|
66
|
+
});
|
|
67
|
+
if (pausedByNodeId)
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (pausedByNodeId)
|
|
71
|
+
break;
|
|
72
|
+
await mapConcurrent(regularRunnable, input.maxConcurrent, async (nodeId) => {
|
|
73
|
+
await input.executeScheduledNode(nodeId, executeNode, (id, pausedAt, pauseReason) => {
|
|
74
|
+
pausedByNodeId = id;
|
|
75
|
+
input.state.pausedAt = pausedAt;
|
|
76
|
+
input.state.pausedByNodeId = id;
|
|
77
|
+
input.state.pauseReason = pauseReason;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
if (pausedByNodeId)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return pausedByNodeId;
|
|
84
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { writeDagNodeTextArtifact, writeRelocatedDagNodeRecord, } from "../../infrastructure/harness/artifact-store.js";
|
|
5
4
|
import { rewritePathPrefix } from "../../shared/path-refs.js";
|
|
5
|
+
import { MAX_UPSTREAM_CHARS } from "./prompt.js";
|
|
6
6
|
function sha256Text(text) {
|
|
7
7
|
return createHash("sha256").update(text).digest("hex");
|
|
8
8
|
}
|
|
@@ -11,10 +11,7 @@ export async function persistLongNodeOutputArtifacts(input) {
|
|
|
11
11
|
const nodeRecordPath = path.join(input.runDir, `${input.nodeId}.json`);
|
|
12
12
|
const meta = { nodeRecordPath };
|
|
13
13
|
if (input.stdout && input.stdout.length > previewChars) {
|
|
14
|
-
const
|
|
15
|
-
await mkdir(nodeDir, { recursive: true });
|
|
16
|
-
const stdoutPath = path.join(nodeDir, "stdout.md");
|
|
17
|
-
await writeFile(stdoutPath, input.stdout, "utf-8");
|
|
14
|
+
const stdoutPath = await writeDagNodeTextArtifact(input.runDir, input.nodeId, "stdout.md", input.stdout);
|
|
18
15
|
meta.stdoutArtifactPath = stdoutPath;
|
|
19
16
|
meta.stdoutChars = input.stdout.length;
|
|
20
17
|
meta.stdoutSha256 = sha256Text(input.stdout);
|
|
@@ -23,10 +20,7 @@ export async function persistLongNodeOutputArtifacts(input) {
|
|
|
23
20
|
if (assistant &&
|
|
24
21
|
assistant !== input.stdout &&
|
|
25
22
|
assistant.length > previewChars) {
|
|
26
|
-
const
|
|
27
|
-
await mkdir(nodeDir, { recursive: true });
|
|
28
|
-
const assistantPath = path.join(nodeDir, "assistant.md");
|
|
29
|
-
await writeFile(assistantPath, assistant, "utf-8");
|
|
23
|
+
const assistantPath = await writeDagNodeTextArtifact(input.runDir, input.nodeId, "assistant.md", assistant);
|
|
30
24
|
meta.assistantArtifactPath = assistantPath;
|
|
31
25
|
meta.assistantChars = assistant.length;
|
|
32
26
|
meta.assistantSha256 = sha256Text(assistant);
|
|
@@ -39,25 +33,33 @@ export function relocateNodeArtifactPaths(node, oldRunDir, newRunDir) {
|
|
|
39
33
|
const rewrite = (value) => {
|
|
40
34
|
if (!value)
|
|
41
35
|
return value;
|
|
42
|
-
|
|
43
|
-
if (rewritten !== value)
|
|
44
|
-
return rewritten;
|
|
45
|
-
if (value === oldRunDir || value.startsWith(`${oldRunDir}${path.sep}`)) {
|
|
46
|
-
return path.join(newRunDir, path.relative(oldRunDir, value));
|
|
47
|
-
}
|
|
48
|
-
return value;
|
|
36
|
+
return rewritePathPrefix(value, oldRunDir, newRunDir);
|
|
49
37
|
};
|
|
50
38
|
node.stdoutArtifactPath = rewrite(node.stdoutArtifactPath);
|
|
51
39
|
node.assistantArtifactPath = rewrite(node.assistantArtifactPath);
|
|
52
40
|
node.nodeRecordPath = rewrite(node.nodeRecordPath);
|
|
53
41
|
}
|
|
42
|
+
export function relocateConvergenceArtifactPaths(convergence, oldRunDir, newRunDir) {
|
|
43
|
+
if (oldRunDir === newRunDir)
|
|
44
|
+
return;
|
|
45
|
+
const relocate = (value) => value ? rewritePathPrefix(value, oldRunDir, newRunDir) : value;
|
|
46
|
+
for (const pass of convergence.passHistory) {
|
|
47
|
+
for (const ref of pass.artifactRefs) {
|
|
48
|
+
ref.nodeRecordPath = relocate(ref.nodeRecordPath);
|
|
49
|
+
ref.stdoutArtifactPath = relocate(ref.stdoutArtifactPath);
|
|
50
|
+
ref.assistantArtifactPath = relocate(ref.assistantArtifactPath);
|
|
51
|
+
ref.preservedNodeRecordPath = relocate(ref.preservedNodeRecordPath);
|
|
52
|
+
ref.preservedNodeDir = relocate(ref.preservedNodeDir);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
54
56
|
export async function relocateRunArtifactPaths(input) {
|
|
55
57
|
const { runDir, oldRunDir, nodes } = input;
|
|
56
58
|
if (oldRunDir === runDir)
|
|
57
59
|
return;
|
|
58
60
|
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
59
61
|
relocateNodeArtifactPaths(node, oldRunDir, runDir);
|
|
60
|
-
await
|
|
62
|
+
await writeRelocatedDagNodeRecord(runDir, nodeId, node);
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
export function formatStdoutPreview(stdout, maxChars) {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFile } 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 { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "../../../executors/shell-write-guard.js";
|
|
5
|
+
import { computeRunDir, persistCursorRunLog } from "../../../records/one-shot-runs.js";
|
|
6
|
+
import { writeLoopCursorFixRecord } from "../../../infrastructure/harness/loop-action-store.js";
|
|
7
|
+
import { loadTaskConfig } from "../../../task/runtime.js";
|
|
8
|
+
import { getLoopPaths } from "../paths.js";
|
|
9
|
+
import { appendLoopRound } from "../rounds.js";
|
|
10
|
+
import { rewriteLoopContext } from "../context.js";
|
|
11
|
+
import { readLoopRounds } from "../rounds.js";
|
|
12
|
+
import { validateLoopCursorFixPolicy } from "../policy/cursor-fix-policy.js";
|
|
13
|
+
import { filterLoopOwnedChangedPaths, hasDagRound, relativeRef } from "./shared.js";
|
|
14
|
+
import { readLatestPiReviewRecord } from "./pi-review.js";
|
|
15
|
+
function extractChangedPaths(result) {
|
|
16
|
+
const raw = result.details?.changedPaths;
|
|
17
|
+
if (!Array.isArray(raw))
|
|
18
|
+
return undefined;
|
|
19
|
+
return raw.filter((value) => typeof value === "string");
|
|
20
|
+
}
|
|
21
|
+
export async function buildLoopCursorFixPrompt(repoRoot, taskId) {
|
|
22
|
+
const paths = getLoopPaths(repoRoot, taskId);
|
|
23
|
+
const [objective, context] = await Promise.all([
|
|
24
|
+
readFile(paths.objectivePath, "utf-8"),
|
|
25
|
+
readFile(paths.contextPath, "utf-8"),
|
|
26
|
+
]);
|
|
27
|
+
const config = await loadTaskConfig(repoRoot, taskId);
|
|
28
|
+
const rounds = await readLoopRounds(repoRoot, taskId);
|
|
29
|
+
const latestReview = await readLatestPiReviewRecord(repoRoot, taskId);
|
|
30
|
+
validateLoopCursorFixPolicy({
|
|
31
|
+
allowedPaths: config.allowedPaths,
|
|
32
|
+
forbiddenPaths: config.forbiddenPaths,
|
|
33
|
+
complexity: config.complexity,
|
|
34
|
+
dagFallbackReason: config.dagFallbackReason ?? "",
|
|
35
|
+
hasDagRound: hasDagRound(rounds),
|
|
36
|
+
});
|
|
37
|
+
return [
|
|
38
|
+
`Task id: ${taskId}`,
|
|
39
|
+
"Exact objective: implement the next bounded fix described by the loop context.",
|
|
40
|
+
"",
|
|
41
|
+
"Allowed paths:",
|
|
42
|
+
...config.allowedPaths.map((entry) => `- ${entry}`),
|
|
43
|
+
"",
|
|
44
|
+
"Forbidden paths:",
|
|
45
|
+
...(config.forbiddenPaths.length > 0
|
|
46
|
+
? config.forbiddenPaths.map((entry) => `- ${entry}`)
|
|
47
|
+
: ["- none"]),
|
|
48
|
+
"",
|
|
49
|
+
"Hard constraints:",
|
|
50
|
+
"- Preserve unrelated files and user work.",
|
|
51
|
+
"- Do not touch forbidden paths.",
|
|
52
|
+
"- Do not mark the loop complete; shell verification or review must follow.",
|
|
53
|
+
"",
|
|
54
|
+
"Expected verification:",
|
|
55
|
+
"- Run the verification commands listed in the task constraints after the fix, or explain why they were not run.",
|
|
56
|
+
"",
|
|
57
|
+
"<latest_pi_review>",
|
|
58
|
+
latestReview
|
|
59
|
+
? [
|
|
60
|
+
`recommendedAction: ${latestReview.structured.recommendedAction}`,
|
|
61
|
+
`rootCause: ${latestReview.structured.rootCause}`,
|
|
62
|
+
`fixScope: ${latestReview.structured.fixScope.join(", ")}`,
|
|
63
|
+
`invariant: ${latestReview.structured.invariant ?? "unknown"}`,
|
|
64
|
+
].join("\n")
|
|
65
|
+
: "No structured Pi review record found.",
|
|
66
|
+
"</latest_pi_review>",
|
|
67
|
+
"",
|
|
68
|
+
"<objective>",
|
|
69
|
+
objective.trim(),
|
|
70
|
+
"</objective>",
|
|
71
|
+
"",
|
|
72
|
+
"<context>",
|
|
73
|
+
context.trim(),
|
|
74
|
+
"</context>",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
async function defaultCursorFixExecutor(input) {
|
|
78
|
+
return executeCursorTask({
|
|
79
|
+
task: input.prompt,
|
|
80
|
+
cwd: input.cwd,
|
|
81
|
+
model: input.model,
|
|
82
|
+
timeoutMs: input.timeoutMs,
|
|
83
|
+
runDir: input.runDir,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export async function runLoopCursorFix(repoRoot, taskId, options = {}) {
|
|
87
|
+
const config = await loadTaskConfig(repoRoot, taskId);
|
|
88
|
+
const rounds = await readLoopRounds(repoRoot, taskId);
|
|
89
|
+
validateLoopCursorFixPolicy({
|
|
90
|
+
allowedPaths: config.allowedPaths,
|
|
91
|
+
forbiddenPaths: config.forbiddenPaths,
|
|
92
|
+
complexity: config.complexity,
|
|
93
|
+
dagFallbackReason: config.dagFallbackReason ?? "",
|
|
94
|
+
hasDagRound: hasDagRound(rounds),
|
|
95
|
+
});
|
|
96
|
+
const model = options.model ?? config.cursorModel ?? DEFAULT_CURSOR_MODEL;
|
|
97
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CURSOR_TIMEOUT_MS;
|
|
98
|
+
const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
|
|
99
|
+
const executor = options.executor ?? defaultCursorFixExecutor;
|
|
100
|
+
const round = rounds.length + 1;
|
|
101
|
+
const latestReview = await readLatestPiReviewRecord(repoRoot, taskId);
|
|
102
|
+
const prompt = await buildLoopCursorFixPrompt(repoRoot, taskId);
|
|
103
|
+
const runDir = (await computeRunDir(repoRoot, `${taskId} cursor-fix r${round}`)) ?? undefined;
|
|
104
|
+
const beforeStatus = await readGitStatusPorcelain(repoRoot).catch(() => "");
|
|
105
|
+
const result = await executor({ prompt, cwd, model, timeoutMs, runDir });
|
|
106
|
+
const explicitChangedPaths = extractChangedPaths(result);
|
|
107
|
+
let changedPaths = explicitChangedPaths
|
|
108
|
+
? filterLoopOwnedChangedPaths(explicitChangedPaths, repoRoot, runDir)
|
|
109
|
+
: undefined;
|
|
110
|
+
const guard = explicitChangedPaths
|
|
111
|
+
? validateShellWriteGuard({
|
|
112
|
+
changedPaths: changedPaths ?? [],
|
|
113
|
+
writePolicy: "exclusive",
|
|
114
|
+
writeSet: config.allowedPaths,
|
|
115
|
+
forbiddenPaths: config.forbiddenPaths,
|
|
116
|
+
})
|
|
117
|
+
: await (async () => {
|
|
118
|
+
const afterStatus = await readGitStatusPorcelain(repoRoot).catch(() => "");
|
|
119
|
+
changedPaths = filterLoopOwnedChangedPaths(pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), snapshotGitStatusPorcelain(afterStatus)), repoRoot, runDir);
|
|
120
|
+
return validateShellWriteGuard({
|
|
121
|
+
changedPaths,
|
|
122
|
+
writePolicy: "exclusive",
|
|
123
|
+
writeSet: config.allowedPaths,
|
|
124
|
+
forbiddenPaths: config.forbiddenPaths,
|
|
125
|
+
});
|
|
126
|
+
})();
|
|
127
|
+
const finalResult = guard.ok
|
|
128
|
+
? result
|
|
129
|
+
: {
|
|
130
|
+
...result,
|
|
131
|
+
ok: false,
|
|
132
|
+
status: "failed",
|
|
133
|
+
failureCategory: "path-violation",
|
|
134
|
+
stderr: [
|
|
135
|
+
result.stderr,
|
|
136
|
+
`loop cursor-fix path violations: ${guard.violations.join(", ")}`,
|
|
137
|
+
]
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join("\n"),
|
|
140
|
+
};
|
|
141
|
+
const persisted = await persistCursorRunLog(repoRoot, {
|
|
142
|
+
task: prompt,
|
|
143
|
+
model,
|
|
144
|
+
channel: "tool",
|
|
145
|
+
cwd,
|
|
146
|
+
result: finalResult,
|
|
147
|
+
taskId,
|
|
148
|
+
runDir,
|
|
149
|
+
});
|
|
150
|
+
const runRef = persisted?.logPath
|
|
151
|
+
? relativeRef(repoRoot, persisted.logPath)
|
|
152
|
+
: "";
|
|
153
|
+
const paths = getLoopPaths(repoRoot, taskId);
|
|
154
|
+
const record = {
|
|
155
|
+
schemaVersion: 1,
|
|
156
|
+
taskId,
|
|
157
|
+
round,
|
|
158
|
+
action: "cursor-fix",
|
|
159
|
+
ok: finalResult.ok,
|
|
160
|
+
model,
|
|
161
|
+
runRef,
|
|
162
|
+
allowedPaths: config.allowedPaths,
|
|
163
|
+
forbiddenPaths: config.forbiddenPaths,
|
|
164
|
+
changedPaths: changedPaths ?? [],
|
|
165
|
+
writeGuardViolations: guard.violations,
|
|
166
|
+
fixScope: latestReview?.structured.fixScope ?? [],
|
|
167
|
+
rootCause: latestReview?.structured.rootCause ?? "",
|
|
168
|
+
invariant: latestReview?.structured.invariant ?? "",
|
|
169
|
+
expectedNextVerification: "loop shell-verify or required task verification commands",
|
|
170
|
+
recordedAt: new Date().toISOString(),
|
|
171
|
+
};
|
|
172
|
+
const recordPath = path.join(paths.reviewDir, `round-${round}-cursor-fix.json`);
|
|
173
|
+
await writeLoopCursorFixRecord(repoRoot, taskId, round, record);
|
|
174
|
+
await appendLoopRound(repoRoot, taskId, {
|
|
175
|
+
action: "cursor-fix",
|
|
176
|
+
refs: [runRef, relativeRef(repoRoot, recordPath)].filter(Boolean),
|
|
177
|
+
result: finalResult.ok ? "cursor fix completed" : "cursor fix failed",
|
|
178
|
+
verification: finalResult.ok
|
|
179
|
+
? "cursor-fix completed; shell verification still required"
|
|
180
|
+
: `cursor-fix failed: ${finalResult.failureCategory}`,
|
|
181
|
+
lesson: finalResult.ok
|
|
182
|
+
? "cursor-fix changed only allowed paths; completion remains gated by verification"
|
|
183
|
+
: finalResult.stderr || finalResult.stdout || "cursor-fix failed",
|
|
184
|
+
next: "run shell verification for the cursor fix",
|
|
185
|
+
decision: "continue",
|
|
186
|
+
failureCategory: finalResult.ok ? undefined : finalResult.failureCategory,
|
|
187
|
+
completedCriteria: finalResult.ok ? ["cursor bounded fix action executed"] : [],
|
|
188
|
+
});
|
|
189
|
+
await rewriteLoopContext(repoRoot, taskId);
|
|
190
|
+
return record;
|
|
191
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { writeLoopDagRecord } from "../../../infrastructure/harness/loop-action-store.js";
|
|
4
|
+
import { getLoopPaths } from "../paths.js";
|
|
5
|
+
import { appendLoopRound } from "../rounds.js";
|
|
6
|
+
import { rewriteLoopContext } from "../context.js";
|
|
7
|
+
import { readLoopRounds } from "../rounds.js";
|
|
8
|
+
import { asRecord, dagFailureCategory, firstReportRun, loopDecisionForDagRun, loopResultForDagRun, relativeRef, stringField, } from "./shared.js";
|
|
9
|
+
let defaultDagCommandRunner;
|
|
10
|
+
export function configureLoopDagDefaultRunner(runner) {
|
|
11
|
+
defaultDagCommandRunner = runner;
|
|
12
|
+
}
|
|
13
|
+
function resolveDefaultRunner(repoRoot) {
|
|
14
|
+
if (!defaultDagCommandRunner) {
|
|
15
|
+
throw new Error("Loop DAG action default runner is not configured");
|
|
16
|
+
}
|
|
17
|
+
return (command, args) => defaultDagCommandRunner(repoRoot, command, args);
|
|
18
|
+
}
|
|
19
|
+
export async function runLoopDagAction(repoRoot, taskId, options = {}) {
|
|
20
|
+
const mode = options.mode ?? "review";
|
|
21
|
+
const cwd = options.cwd ? path.resolve(repoRoot, options.cwd) : repoRoot;
|
|
22
|
+
const rounds = await readLoopRounds(repoRoot, taskId);
|
|
23
|
+
const round = rounds.length + 1;
|
|
24
|
+
const paths = getLoopPaths(repoRoot, taskId);
|
|
25
|
+
const dagEvidenceDir = path.join(paths.loopDir, "dag");
|
|
26
|
+
await mkdir(dagEvidenceDir, { recursive: true });
|
|
27
|
+
const dagPath = options.dagPath
|
|
28
|
+
? path.resolve(repoRoot, options.dagPath)
|
|
29
|
+
: path.join(dagEvidenceDir, `round-${round}.dag.json`);
|
|
30
|
+
const runner = options.runner ?? resolveDefaultRunner(repoRoot);
|
|
31
|
+
let generated = {};
|
|
32
|
+
if (!options.dagPath) {
|
|
33
|
+
generated = asRecord(await runner("dag-run-task", [
|
|
34
|
+
taskId,
|
|
35
|
+
"--profile",
|
|
36
|
+
"auto",
|
|
37
|
+
"--strict-models",
|
|
38
|
+
"--output",
|
|
39
|
+
dagPath,
|
|
40
|
+
"--cwd",
|
|
41
|
+
cwd,
|
|
42
|
+
]), "dag run-task");
|
|
43
|
+
}
|
|
44
|
+
await runner("dag-validate", [
|
|
45
|
+
"--dag",
|
|
46
|
+
dagPath,
|
|
47
|
+
"--strict-models",
|
|
48
|
+
"--strict-governance",
|
|
49
|
+
]);
|
|
50
|
+
let runId = "";
|
|
51
|
+
let runStatus = "not-run";
|
|
52
|
+
let runLifecycle = "not-run";
|
|
53
|
+
let runRef = "";
|
|
54
|
+
let reportRef = "";
|
|
55
|
+
let failureCategory = "";
|
|
56
|
+
let decision = "continue";
|
|
57
|
+
let result = "dag generated and strict-governance validated";
|
|
58
|
+
let lesson = "DAG review packet is ready; inspect writeSet, gates, and expected verification before execution.";
|
|
59
|
+
let completedCriteria = ["dag generated and strict-governance validated"];
|
|
60
|
+
if (mode === "execute") {
|
|
61
|
+
const runArgs = ["--dag", dagPath, "--cwd", cwd];
|
|
62
|
+
if (options.runId)
|
|
63
|
+
runArgs.push("--run-id", options.runId);
|
|
64
|
+
if (options.maxConcurrent !== undefined) {
|
|
65
|
+
runArgs.push("--max-concurrent", String(options.maxConcurrent));
|
|
66
|
+
}
|
|
67
|
+
if (options.noCursor)
|
|
68
|
+
runArgs.push("--no-cursor");
|
|
69
|
+
const runSummary = asRecord(await runner("run-dag", runArgs), "run-dag");
|
|
70
|
+
runId = stringField(runSummary, "runId", "run-dag");
|
|
71
|
+
const report = await runner("dag-report", [
|
|
72
|
+
"--run-id",
|
|
73
|
+
runId,
|
|
74
|
+
"--json",
|
|
75
|
+
"--lifecycle",
|
|
76
|
+
"all",
|
|
77
|
+
]);
|
|
78
|
+
const run = firstReportRun(report);
|
|
79
|
+
runStatus = stringField(run, "status", "dag report run");
|
|
80
|
+
runLifecycle = stringField(run, "lifecycle", "dag report run");
|
|
81
|
+
runRef = relativeRef(repoRoot, stringField(run, "runDir", "dag report run"));
|
|
82
|
+
failureCategory = dagFailureCategory(run);
|
|
83
|
+
decision = loopDecisionForDagRun(run);
|
|
84
|
+
result = loopResultForDagRun(run);
|
|
85
|
+
lesson =
|
|
86
|
+
decision === "pause"
|
|
87
|
+
? "DAG run paused for human approval; use DAG decision commands before resuming."
|
|
88
|
+
: "DAG run facts recorded; inspect dag report and follow with shell verification or review.";
|
|
89
|
+
completedCriteria = ["dag round action executed"];
|
|
90
|
+
}
|
|
91
|
+
const recordPath = path.join(dagEvidenceDir, `round-${round}.json`);
|
|
92
|
+
reportRef = relativeRef(repoRoot, recordPath);
|
|
93
|
+
const record = {
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
taskId,
|
|
96
|
+
round,
|
|
97
|
+
action: "dag",
|
|
98
|
+
mode,
|
|
99
|
+
ok: mode === "review" || runStatus === "finished",
|
|
100
|
+
dagPath: relativeRef(repoRoot, dagPath),
|
|
101
|
+
strictValidateOk: true,
|
|
102
|
+
runId,
|
|
103
|
+
runStatus,
|
|
104
|
+
runLifecycle,
|
|
105
|
+
runRef,
|
|
106
|
+
reportRef,
|
|
107
|
+
reviewPacket: generated.reviewPacket,
|
|
108
|
+
failureCategory,
|
|
109
|
+
expectedNextVerification: decision === "pause"
|
|
110
|
+
? "approve or reject paused DAG run, then resume or replan"
|
|
111
|
+
: "run shell verification or review DAG report before completion",
|
|
112
|
+
recordedAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
await writeLoopDagRecord(repoRoot, taskId, round, record);
|
|
115
|
+
await appendLoopRound(repoRoot, taskId, {
|
|
116
|
+
action: "dag",
|
|
117
|
+
refs: [runRef, record.dagPath, reportRef].filter(Boolean),
|
|
118
|
+
result,
|
|
119
|
+
verification: mode === "review"
|
|
120
|
+
? "dag generated and validated with --strict-models --strict-governance"
|
|
121
|
+
: `dag report status=${runStatus}, lifecycle=${runLifecycle}`,
|
|
122
|
+
lesson,
|
|
123
|
+
next: record.expectedNextVerification,
|
|
124
|
+
decision,
|
|
125
|
+
failureCategory,
|
|
126
|
+
completedCriteria,
|
|
127
|
+
});
|
|
128
|
+
await rewriteLoopContext(repoRoot, taskId);
|
|
129
|
+
return record;
|
|
130
|
+
}
|