@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,185 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeDagRunJsonArtifact } from "../../../infrastructure/harness/artifact-store.js";
|
|
4
|
+
import { executeDagNode, } from "../node-execution.js";
|
|
5
|
+
import { writeRunSpec } from "../run-store.js";
|
|
6
|
+
import { freshNodeRecord, parseJsonFromText, renderDynamicPatternList, renderDynamicTemplate, resolveItemsFromSelector, sha256Json, } from "./shared.js";
|
|
7
|
+
export function buildExpandedChildTask(input) {
|
|
8
|
+
const { parent, expansion, item, index, nodeId } = input;
|
|
9
|
+
const child = expansion.childTask;
|
|
10
|
+
const subtaskPrompt = renderDynamicTemplate(child.subtaskPromptTemplate, item, index, expansion.itemName);
|
|
11
|
+
const staticResult = child.staticResultTemplate
|
|
12
|
+
? renderDynamicTemplate(child.staticResultTemplate, item, index, expansion.itemName)
|
|
13
|
+
: undefined;
|
|
14
|
+
return {
|
|
15
|
+
id: nodeId,
|
|
16
|
+
depends_on: parent.depends_on,
|
|
17
|
+
complexity: child.complexity,
|
|
18
|
+
subtask_prompt: subtaskPrompt,
|
|
19
|
+
executor: child.executor,
|
|
20
|
+
role: child.role,
|
|
21
|
+
writePolicy: child.writePolicy,
|
|
22
|
+
allowedPaths: renderDynamicPatternList(child.allowedPaths, item, index, expansion.itemName) ?? [],
|
|
23
|
+
forbiddenPaths: renderDynamicPatternList(child.forbiddenPaths, item, index, expansion.itemName) ?? [],
|
|
24
|
+
writeSet: renderDynamicPatternList(child.writeSet, item, index, expansion.itemName),
|
|
25
|
+
outputContract: child.outputContract,
|
|
26
|
+
static: child.executor === "static"
|
|
27
|
+
? { resultMarkdown: staticResult ?? "dynamic child completed" }
|
|
28
|
+
: undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function addExpansionRank(input) {
|
|
32
|
+
if (input.state.ranks.some((rank) => input.childNodeIds.every((nodeId) => rank.includes(nodeId)))) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const parentRankIndex = input.state.ranks.findIndex((rank) => rank.includes(input.parentNodeId));
|
|
36
|
+
const insertAt = parentRankIndex >= 0 ? parentRankIndex + 1 : input.state.ranks.length;
|
|
37
|
+
input.state.ranks.splice(insertAt, 0, input.childNodeIds);
|
|
38
|
+
}
|
|
39
|
+
export function normalizedWritePattern(pattern) {
|
|
40
|
+
return pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
41
|
+
}
|
|
42
|
+
export function writePatternsOverlap(left, right) {
|
|
43
|
+
const normalizedLeft = normalizedWritePattern(left);
|
|
44
|
+
const normalizedRight = normalizedWritePattern(right);
|
|
45
|
+
if (normalizedLeft === normalizedRight)
|
|
46
|
+
return true;
|
|
47
|
+
const leftPrefix = normalizedLeft.replace(/\*\*.*$/, "");
|
|
48
|
+
const rightPrefix = normalizedRight.replace(/\*\*.*$/, "");
|
|
49
|
+
return (leftPrefix.length > 0 &&
|
|
50
|
+
rightPrefix.length > 0 &&
|
|
51
|
+
(leftPrefix.startsWith(rightPrefix) || rightPrefix.startsWith(leftPrefix)));
|
|
52
|
+
}
|
|
53
|
+
export function collectDynamicChildWriteSetConflicts(children) {
|
|
54
|
+
const conflicts = [];
|
|
55
|
+
const exclusiveChildren = children.filter((child) => child.writePolicy === "exclusive" && (child.writeSet?.length ?? 0) > 0);
|
|
56
|
+
for (let i = 0; i < exclusiveChildren.length; i += 1) {
|
|
57
|
+
for (let j = i + 1; j < exclusiveChildren.length; j += 1) {
|
|
58
|
+
const left = exclusiveChildren[i];
|
|
59
|
+
const right = exclusiveChildren[j];
|
|
60
|
+
for (const leftEntry of left.writeSet ?? []) {
|
|
61
|
+
for (const rightEntry of right.writeSet ?? []) {
|
|
62
|
+
if (writePatternsOverlap(leftEntry, rightEntry)) {
|
|
63
|
+
conflicts.push(`${left.id}:${leftEntry} overlaps ${right.id}:${rightEntry}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return conflicts;
|
|
70
|
+
}
|
|
71
|
+
export function resolveRunLocalPath(runDir, ref, label) {
|
|
72
|
+
if (path.isAbsolute(ref)) {
|
|
73
|
+
throw new Error(`${label} must be relative to the DAG run directory: ${ref}`);
|
|
74
|
+
}
|
|
75
|
+
const runRoot = path.resolve(runDir);
|
|
76
|
+
const resolved = path.resolve(runRoot, ref);
|
|
77
|
+
if (resolved !== runRoot && !resolved.startsWith(`${runRoot}${path.sep}`)) {
|
|
78
|
+
throw new Error(`${label} escapes the DAG run directory: ${ref}`);
|
|
79
|
+
}
|
|
80
|
+
return resolved;
|
|
81
|
+
}
|
|
82
|
+
export async function executeDynamicMapExpansion(input) {
|
|
83
|
+
const started = Date.now();
|
|
84
|
+
const items = resolveItemsFromSelector(input.expansion.itemsFrom, input.state);
|
|
85
|
+
if (items.length > input.expansion.maxExpandedNodes) {
|
|
86
|
+
throw new Error(`map_agent ${input.task.id} item count ${items.length} exceeds maxExpandedNodes ${input.expansion.maxExpandedNodes}`);
|
|
87
|
+
}
|
|
88
|
+
if (items.length > input.expansion.maxItems) {
|
|
89
|
+
throw new Error(`map_agent ${input.task.id} item count ${items.length} exceeds maxItems ${input.expansion.maxItems}`);
|
|
90
|
+
}
|
|
91
|
+
const childNodeIds = items.map((_, index) => `${input.expansion.childIdPrefix}-${String(index + 1).padStart(4, "0")}`);
|
|
92
|
+
const children = childNodeIds.map((nodeId, index) => buildExpandedChildTask({
|
|
93
|
+
parent: input.task,
|
|
94
|
+
expansion: input.expansion,
|
|
95
|
+
item: items[index],
|
|
96
|
+
index,
|
|
97
|
+
nodeId,
|
|
98
|
+
}));
|
|
99
|
+
const writeSetConflicts = collectDynamicChildWriteSetConflicts(children);
|
|
100
|
+
if (writeSetConflicts.length > 0) {
|
|
101
|
+
throw new Error(`dynamic map_agent ${input.task.id} expanded overlapping writeSets: ${writeSetConflicts.join("; ")}`);
|
|
102
|
+
}
|
|
103
|
+
const workspaceRefs = items.map((item, index) => input.expansion.workspaceTemplate
|
|
104
|
+
? renderDynamicTemplate(input.expansion.workspaceTemplate, item, index, input.expansion.itemName)
|
|
105
|
+
: undefined);
|
|
106
|
+
for (const workspaceRef of workspaceRefs) {
|
|
107
|
+
if (!workspaceRef)
|
|
108
|
+
continue;
|
|
109
|
+
await mkdir(resolveRunLocalPath(input.runDir, workspaceRef, "workspaceRef"), {
|
|
110
|
+
recursive: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
for (const child of children) {
|
|
114
|
+
if (!input.tasksById.has(child.id)) {
|
|
115
|
+
input.tasksById.set(child.id, child);
|
|
116
|
+
input.spec.tasks.push(child);
|
|
117
|
+
}
|
|
118
|
+
if (!input.state.nodes[child.id]) {
|
|
119
|
+
input.state.nodes[child.id] = freshNodeRecord(child);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
addExpansionRank({
|
|
123
|
+
state: input.state,
|
|
124
|
+
parentNodeId: input.task.id,
|
|
125
|
+
childNodeIds,
|
|
126
|
+
});
|
|
127
|
+
await writeRunSpec(input.runDir, input.spec);
|
|
128
|
+
const manifest = {
|
|
129
|
+
workflowNodeId: input.expansion.workflowNodeId,
|
|
130
|
+
expandedAt: new Date().toISOString(),
|
|
131
|
+
itemsFrom: input.expansion.itemsFrom,
|
|
132
|
+
itemCount: items.length,
|
|
133
|
+
children: childNodeIds.map((nodeId, index) => ({
|
|
134
|
+
nodeId,
|
|
135
|
+
itemRef: `$.items[${index}]`,
|
|
136
|
+
itemValueHash: sha256Json(items[index]),
|
|
137
|
+
workspaceRef: workspaceRefs[index],
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
await writeDagRunJsonArtifact(input.runDir, `expansions/${input.task.id}.expansion.json`, manifest);
|
|
141
|
+
await input.persistState();
|
|
142
|
+
for (const child of children) {
|
|
143
|
+
const childRecord = input.state.nodes[child.id];
|
|
144
|
+
if (childRecord?.status === "FINISHED")
|
|
145
|
+
continue;
|
|
146
|
+
await executeDagNode({
|
|
147
|
+
nodeId: child.id,
|
|
148
|
+
tasksById: input.tasksById,
|
|
149
|
+
state: input.state,
|
|
150
|
+
spec: input.spec,
|
|
151
|
+
cwd: input.cwd,
|
|
152
|
+
runDir: input.runDir,
|
|
153
|
+
executeNode: input.executeNode,
|
|
154
|
+
executeDynamicNode: input.executeDynamicNode,
|
|
155
|
+
observer: input.observer,
|
|
156
|
+
persistState: input.persistState,
|
|
157
|
+
onPause: () => {
|
|
158
|
+
throw new Error(`dynamic child node ${child.id} requested a human pause; map_agent children do not support pause in v0`);
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const failedChildren = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status !== "FINISHED");
|
|
163
|
+
const aggregate = {
|
|
164
|
+
workflowNodeId: input.expansion.workflowNodeId,
|
|
165
|
+
itemCount: items.length,
|
|
166
|
+
children: childNodeIds.map((nodeId, index) => ({
|
|
167
|
+
nodeId,
|
|
168
|
+
item: items[index],
|
|
169
|
+
workspaceRef: workspaceRefs[index],
|
|
170
|
+
status: input.state.nodes[nodeId]?.status,
|
|
171
|
+
stdout: input.state.nodes[nodeId]?.stdout,
|
|
172
|
+
output: parseJsonFromText(input.state.nodes[nodeId]?.stdout),
|
|
173
|
+
assistantText: input.state.nodes[nodeId]?.assistantText,
|
|
174
|
+
})),
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
ok: failedChildren.length === 0,
|
|
178
|
+
stdout: JSON.stringify(aggregate),
|
|
179
|
+
stderr: failedChildren.length > 0
|
|
180
|
+
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
181
|
+
: "",
|
|
182
|
+
failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
|
|
183
|
+
durationMs: Date.now() - started,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { writeDagNodeJsonArtifact } from "../../../infrastructure/harness/artifact-store.js";
|
|
3
|
+
import { repoRelativePath, toPosixPath } from "../../../shared/path-refs.js";
|
|
4
|
+
import { buildVerifiedFindingsReport, findingSchema, verificationSchema, } from "../../dynamic/artifacts.js";
|
|
5
|
+
import { parseJsonFromText, resolveOutputSelector } from "./shared.js";
|
|
6
|
+
function parseFindingsFromSelector(selector, state) {
|
|
7
|
+
const raw = resolveOutputSelector(selector, state);
|
|
8
|
+
if (!Array.isArray(raw)) {
|
|
9
|
+
throw new Error(`findings selector did not resolve to an array: ${selector}`);
|
|
10
|
+
}
|
|
11
|
+
return raw.map((entry) => {
|
|
12
|
+
const direct = findingSchema.safeParse(entry);
|
|
13
|
+
if (direct.success)
|
|
14
|
+
return direct.data;
|
|
15
|
+
if (entry && typeof entry === "object") {
|
|
16
|
+
const output = entry.output;
|
|
17
|
+
const fromOutput = findingSchema.safeParse(output);
|
|
18
|
+
if (fromOutput.success)
|
|
19
|
+
return fromOutput.data;
|
|
20
|
+
const stdout = entry.stdout;
|
|
21
|
+
if (typeof stdout === "string") {
|
|
22
|
+
const fromStdout = findingSchema.safeParse(parseJsonFromText(stdout));
|
|
23
|
+
if (fromStdout.success)
|
|
24
|
+
return fromStdout.data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return findingSchema.parse(entry);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function parseVerificationFromChildEntry(entry) {
|
|
31
|
+
if (!entry || typeof entry !== "object")
|
|
32
|
+
return undefined;
|
|
33
|
+
const stdout = entry.stdout;
|
|
34
|
+
if (typeof stdout !== "string" || stdout.trim().length === 0) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return verificationSchema.parse(JSON.parse(stdout));
|
|
38
|
+
}
|
|
39
|
+
function parseVerificationsFromSelector(selector, state) {
|
|
40
|
+
const raw = resolveOutputSelector(selector, state);
|
|
41
|
+
if (!Array.isArray(raw)) {
|
|
42
|
+
throw new Error(`verifications selector did not resolve to an array: ${selector}`);
|
|
43
|
+
}
|
|
44
|
+
return raw
|
|
45
|
+
.map(parseVerificationFromChildEntry)
|
|
46
|
+
.filter((entry) => Boolean(entry));
|
|
47
|
+
}
|
|
48
|
+
export async function executeDynamicReduction(input) {
|
|
49
|
+
const started = Date.now();
|
|
50
|
+
if (input.reduction.type !== "verified_findings_report") {
|
|
51
|
+
throw new Error(`unsupported dynamic reduction: ${input.reduction.type}`);
|
|
52
|
+
}
|
|
53
|
+
const findings = parseFindingsFromSelector(input.reduction.findingsFrom, input.state);
|
|
54
|
+
const verifications = parseVerificationsFromSelector(input.reduction.verificationsFrom, input.state);
|
|
55
|
+
const nodeDir = path.join(input.runDir, input.task.id);
|
|
56
|
+
const refutedFindingsPath = path.join(nodeDir, input.reduction.refutedFindingsArtifactName);
|
|
57
|
+
const { report, refutedFindings } = buildVerifiedFindingsReport({
|
|
58
|
+
findings,
|
|
59
|
+
verifications,
|
|
60
|
+
refutedFindingsRef: refutedFindingsPath.startsWith(input.runDir)
|
|
61
|
+
? repoRelativePath(input.runDir, refutedFindingsPath)
|
|
62
|
+
: toPosixPath(refutedFindingsPath),
|
|
63
|
+
});
|
|
64
|
+
await writeDagNodeJsonArtifact(input.runDir, input.task.id, input.reduction.refutedFindingsArtifactName, refutedFindings);
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
stdout: JSON.stringify(report),
|
|
68
|
+
stderr: "",
|
|
69
|
+
failureCategory: "success",
|
|
70
|
+
durationMs: Date.now() - started,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function freshNodeRecord(task) {
|
|
3
|
+
return {
|
|
4
|
+
id: task.id,
|
|
5
|
+
status: "PENDING",
|
|
6
|
+
executor: task.executor,
|
|
7
|
+
complexity: task.complexity,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function sha256Json(value) {
|
|
11
|
+
return `sha256:${createHash("sha256")
|
|
12
|
+
.update(JSON.stringify(value))
|
|
13
|
+
.digest("hex")}`;
|
|
14
|
+
}
|
|
15
|
+
export function getNodeOutputAsJson(state, nodeId) {
|
|
16
|
+
const record = state.nodes[nodeId];
|
|
17
|
+
if (!record || record.status !== "FINISHED") {
|
|
18
|
+
throw new Error(`itemsFrom upstream node "${nodeId}" is not finished`);
|
|
19
|
+
}
|
|
20
|
+
const raw = record.stdout?.trim() || record.assistantText?.trim();
|
|
21
|
+
if (!raw) {
|
|
22
|
+
throw new Error(`itemsFrom upstream node "${nodeId}" has no JSON output`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`itemsFrom upstream node "${nodeId}" output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function resolveOutputSelector(selector, state) {
|
|
32
|
+
const match = selector.match(/^\$\.nodes\[['"]([^'"]+)['"]\]\.output(?:\.(.+))?$/);
|
|
33
|
+
if (!match?.[1]) {
|
|
34
|
+
throw new Error(`unsupported itemsFrom selector: ${selector}`);
|
|
35
|
+
}
|
|
36
|
+
const nodeId = match[1];
|
|
37
|
+
const pathSuffix = match[2];
|
|
38
|
+
let current = getNodeOutputAsJson(state, nodeId);
|
|
39
|
+
if (pathSuffix) {
|
|
40
|
+
for (const segment of pathSuffix.split(".")) {
|
|
41
|
+
if (current &&
|
|
42
|
+
typeof current === "object" &&
|
|
43
|
+
segment in current) {
|
|
44
|
+
current = current[segment];
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`output selector path not found: ${selector} (missing "${segment}")`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
export function resolveItemsFromSelector(selector, state) {
|
|
53
|
+
let current;
|
|
54
|
+
try {
|
|
55
|
+
current = resolveOutputSelector(selector, state);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
if (message.startsWith("output selector path not found:")) {
|
|
60
|
+
throw new Error(`itemsFrom path not found: ${selector}`);
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(current)) {
|
|
65
|
+
throw new Error(`itemsFrom selector did not resolve to an array: ${selector}`);
|
|
66
|
+
}
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
export function lookupPath(value, pathExpression) {
|
|
70
|
+
let current = value;
|
|
71
|
+
for (const segment of pathExpression.split(".")) {
|
|
72
|
+
if (current &&
|
|
73
|
+
typeof current === "object" &&
|
|
74
|
+
segment in current) {
|
|
75
|
+
current = current[segment];
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
export function templateValueToString(value) {
|
|
83
|
+
if (typeof value === "string" ||
|
|
84
|
+
typeof value === "number" ||
|
|
85
|
+
typeof value === "boolean") {
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
if (value === null || value === undefined)
|
|
89
|
+
return "";
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
}
|
|
92
|
+
export function parseConditionLiteral(raw) {
|
|
93
|
+
const trimmed = raw.trim();
|
|
94
|
+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
95
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
96
|
+
return trimmed.slice(1, -1);
|
|
97
|
+
}
|
|
98
|
+
if (trimmed === "true")
|
|
99
|
+
return true;
|
|
100
|
+
if (trimmed === "false")
|
|
101
|
+
return false;
|
|
102
|
+
if (trimmed === "null")
|
|
103
|
+
return null;
|
|
104
|
+
const numeric = Number(trimmed);
|
|
105
|
+
return Number.isFinite(numeric) ? numeric : trimmed;
|
|
106
|
+
}
|
|
107
|
+
export function parseJsonFromText(value) {
|
|
108
|
+
if (!value?.trim())
|
|
109
|
+
return undefined;
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(value);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function renderDynamicTemplate(template, item, index, itemName) {
|
|
118
|
+
const primitive = typeof item === "string" ||
|
|
119
|
+
typeof item === "number" ||
|
|
120
|
+
typeof item === "boolean"
|
|
121
|
+
? String(item)
|
|
122
|
+
: JSON.stringify(item);
|
|
123
|
+
return template
|
|
124
|
+
.replace(/\{\{\s*item\s*\}\}/g, primitive)
|
|
125
|
+
.replace(new RegExp(`\\{\\{\\s*${itemName}\\s*\\}\\}`, "g"), primitive)
|
|
126
|
+
.replace(new RegExp(`\\{\\{\\s*${itemName}\\.([A-Za-z0-9_.-]+)\\s*\\}\\}`, "g"), (_match, pathExpression) => templateValueToString(lookupPath(item, pathExpression)))
|
|
127
|
+
.replace(/\{\{\s*item\.([A-Za-z0-9_.-]+)\s*\}\}/g, (_match, pathExpression) => templateValueToString(lookupPath(item, pathExpression)))
|
|
128
|
+
.replace(/\{\{\s*itemJson\s*\}\}/g, JSON.stringify(item))
|
|
129
|
+
.replace(/\{\{\s*index\s*\}\}/g, String(index));
|
|
130
|
+
}
|
|
131
|
+
export function renderDynamicPatternList(patterns, item, index, itemName) {
|
|
132
|
+
return patterns?.map((pattern) => renderDynamicTemplate(pattern, item, index, itemName));
|
|
133
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { access, mkdir, readFile, readdir, rename,
|
|
1
|
+
import { access, mkdir, readFile, readdir, rename, } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
3
4
|
import { parseDagSpec } from "./types.js";
|
|
4
5
|
const DAG_LIFECYCLE_SCAN_ORDER = [
|
|
5
6
|
"paused",
|
|
@@ -24,8 +25,8 @@ export async function readDagRunState(runDir) {
|
|
|
24
25
|
const raw = JSON.parse(await readFile(path.join(runDir, "state.json"), "utf-8"));
|
|
25
26
|
return raw;
|
|
26
27
|
}
|
|
27
|
-
export async function writeDagRunState(runDir, state) {
|
|
28
|
-
await
|
|
28
|
+
export async function writeDagRunState(runDir, state, options) {
|
|
29
|
+
await writeJsonAtomic(path.join(runDir, "state.json"), state, options);
|
|
29
30
|
}
|
|
30
31
|
export async function readDagRunSpec(runDir) {
|
|
31
32
|
const raw = JSON.parse(await readFile(path.join(runDir, "run.json"), "utf-8"));
|
|
@@ -131,14 +132,14 @@ export async function writeHumanApprovalArtifact(input) {
|
|
|
131
132
|
const nodeDir = path.join(input.runDir, input.nodeId);
|
|
132
133
|
await mkdir(nodeDir, { recursive: true });
|
|
133
134
|
const artifactPath = path.join(nodeDir, "human-approval.json");
|
|
134
|
-
await
|
|
135
|
+
await writeJsonAtomic(artifactPath, input.artifact);
|
|
135
136
|
return artifactPath;
|
|
136
137
|
}
|
|
137
138
|
export async function writeHumanRejectionArtifact(input) {
|
|
138
139
|
const nodeDir = path.join(input.runDir, input.nodeId);
|
|
139
140
|
await mkdir(nodeDir, { recursive: true });
|
|
140
141
|
const artifactPath = path.join(nodeDir, "human-rejection.json");
|
|
141
|
-
await
|
|
142
|
+
await writeJsonAtomic(artifactPath, input.artifact);
|
|
142
143
|
return artifactPath;
|
|
143
144
|
}
|
|
144
145
|
export async function readHumanApprovalArtifact(runDir, nodeId) {
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
|
|
3
|
+
import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
4
|
+
import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
5
|
+
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
6
|
+
import { resolveDagSkillInstructions, skillInstructionMetadata, } from "./skill-instructions.js";
|
|
7
|
+
import { resolveDagNodeSkills } from "./skills.js";
|
|
8
|
+
import { parseRepairArtifactFromText, validateRepairArtifactScope, } from "./repair-artifact.js";
|
|
9
|
+
import { resolveModelForTask, } from "./types.js";
|
|
10
|
+
export function buildNodePrompt(spec, task, upstream) {
|
|
11
|
+
return buildDagNodePromptEnvelope({
|
|
12
|
+
spec,
|
|
13
|
+
task,
|
|
14
|
+
upstream,
|
|
15
|
+
resolvedSkills: resolveDagNodeSkills(spec, task),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
|
|
19
|
+
const skillNames = resolveDagNodeSkills(spec, task);
|
|
20
|
+
const resolvedSkillInstructions = task.executor === "cursor" || task.executor === "pi"
|
|
21
|
+
? await resolveDagSkillInstructions(skillNames, {
|
|
22
|
+
cwd,
|
|
23
|
+
includeLearnedPatterns: task.role === "implementer",
|
|
24
|
+
})
|
|
25
|
+
: [];
|
|
26
|
+
return {
|
|
27
|
+
prompt: buildDagNodePromptEnvelope({
|
|
28
|
+
spec,
|
|
29
|
+
task,
|
|
30
|
+
upstream,
|
|
31
|
+
resolvedSkills: skillNames,
|
|
32
|
+
resolvedSkillInstructions,
|
|
33
|
+
}),
|
|
34
|
+
resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function parseProcessVerdict(node) {
|
|
38
|
+
const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
|
|
39
|
+
for (const line of text.split("\n")) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (trimmed === "VERDICT: pass")
|
|
42
|
+
return "pass";
|
|
43
|
+
if (trimmed === "VERDICT: request-revision")
|
|
44
|
+
return "request-revision";
|
|
45
|
+
}
|
|
46
|
+
return "unknown";
|
|
47
|
+
}
|
|
48
|
+
function assertRepairArtifactVerdictMatchesSupervisor(input) {
|
|
49
|
+
const supervisorVerdict = parseProcessVerdict(input.node);
|
|
50
|
+
if (supervisorVerdict === "unknown")
|
|
51
|
+
return;
|
|
52
|
+
if (supervisorVerdict !== input.artifactVerdict) {
|
|
53
|
+
throw new Error(`repair artifact gate failed: verdict mismatch between supervisor ${supervisorVerdict} and REPAIR_ARTIFACT_JSON ${input.artifactVerdict}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function findRepairTaskForGate(input) {
|
|
57
|
+
return Array.from(input.tasksById.values()).find((candidate) => candidate.depends_on.includes(input.gateTask.id) &&
|
|
58
|
+
candidate.id === "repair-cursor");
|
|
59
|
+
}
|
|
60
|
+
function parseSupervisorRepairArtifact(node) {
|
|
61
|
+
const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
|
|
62
|
+
return parseRepairArtifactFromText(text);
|
|
63
|
+
}
|
|
64
|
+
function validateRepairArtifactGateBeforeShell(input) {
|
|
65
|
+
const gate = input.task.shell?.repairArtifactGate;
|
|
66
|
+
if (!gate)
|
|
67
|
+
return;
|
|
68
|
+
const upstream = input.state.nodes[gate.fromNodeId];
|
|
69
|
+
const parsed = parseSupervisorRepairArtifact(upstream);
|
|
70
|
+
if (!parsed.ok) {
|
|
71
|
+
throw new Error(`repair artifact gate failed: ${parsed.reason}`);
|
|
72
|
+
}
|
|
73
|
+
assertRepairArtifactVerdictMatchesSupervisor({
|
|
74
|
+
node: upstream,
|
|
75
|
+
artifactVerdict: parsed.artifact.verdict,
|
|
76
|
+
});
|
|
77
|
+
upstream.repairArtifact = parsed.artifact;
|
|
78
|
+
const scoped = validateRepairArtifactScope({
|
|
79
|
+
artifact: parsed.artifact,
|
|
80
|
+
repairTask: findRepairTaskForGate({
|
|
81
|
+
tasksById: input.tasksById,
|
|
82
|
+
gateTask: input.task,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
if (!scoped.ok) {
|
|
86
|
+
throw new Error(`repair artifact gate failed: ${scoped.reason}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function recordRepairArtifactForSupervisorNode(input) {
|
|
90
|
+
const dependentGate = Object.values(input.state.nodes).find((record) => record.id === "process-gate-shell");
|
|
91
|
+
if (input.task.id !== "process-supervisor-pi" || !dependentGate)
|
|
92
|
+
return;
|
|
93
|
+
const parsed = parseSupervisorRepairArtifact(input.node);
|
|
94
|
+
if (parsed.ok) {
|
|
95
|
+
input.node.repairArtifact = parsed.artifact;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function notifyNodeObserver(observer, event, nodeId, state, chunk) {
|
|
99
|
+
try {
|
|
100
|
+
if (event === "onNodeOutput") {
|
|
101
|
+
await observer?.onNodeOutput?.(nodeId, chunk ?? "", state);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await observer?.[event]?.(nodeId, state);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Observers are derived views; they must not affect canonical DAG execution.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function executeDagNode(input) {
|
|
111
|
+
const { nodeId, tasksById, state, spec, cwd, runDir, executeNode } = input;
|
|
112
|
+
const task = tasksById.get(nodeId);
|
|
113
|
+
const node = state.nodes[nodeId];
|
|
114
|
+
node.status = "RUNNING";
|
|
115
|
+
node.startedAt = new Date().toISOString();
|
|
116
|
+
if (task.shell?.verifyEvidence) {
|
|
117
|
+
node.verifyEvidence = task.shell.verifyEvidence;
|
|
118
|
+
}
|
|
119
|
+
await input.persistState();
|
|
120
|
+
await notifyNodeObserver(input.observer, "onNodeStart", nodeId, state);
|
|
121
|
+
if (task.dynamicExpansion ||
|
|
122
|
+
task.dynamicReduction ||
|
|
123
|
+
task.dynamicCondition ||
|
|
124
|
+
task.dynamicLoopUntil) {
|
|
125
|
+
const started = Date.now();
|
|
126
|
+
try {
|
|
127
|
+
const result = await input.executeDynamicNode({
|
|
128
|
+
task,
|
|
129
|
+
tasksById,
|
|
130
|
+
state,
|
|
131
|
+
spec,
|
|
132
|
+
cwd,
|
|
133
|
+
runDir,
|
|
134
|
+
executeNode,
|
|
135
|
+
observer: input.observer,
|
|
136
|
+
persistState: input.persistState,
|
|
137
|
+
});
|
|
138
|
+
node.durationMs = result.durationMs ?? Date.now() - started;
|
|
139
|
+
node.stdout = result.stdout;
|
|
140
|
+
node.stderr = result.stderr;
|
|
141
|
+
node.failureCategory = result.failureCategory;
|
|
142
|
+
node.finishedAt = new Date().toISOString();
|
|
143
|
+
node.status = result.ok ? "FINISHED" : "ERROR";
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
node.status = "ERROR";
|
|
147
|
+
node.stderr = error instanceof Error ? error.message : String(error);
|
|
148
|
+
node.finishedAt = new Date().toISOString();
|
|
149
|
+
node.durationMs = Date.now() - started;
|
|
150
|
+
}
|
|
151
|
+
if (node.status === "FINISHED") {
|
|
152
|
+
const artifactMeta = await persistLongNodeOutputArtifacts({
|
|
153
|
+
runDir,
|
|
154
|
+
nodeId,
|
|
155
|
+
stdout: node.stdout,
|
|
156
|
+
assistantText: node.assistantText,
|
|
157
|
+
});
|
|
158
|
+
Object.assign(node, artifactMeta);
|
|
159
|
+
}
|
|
160
|
+
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
161
|
+
await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
|
|
162
|
+
await input.persistState();
|
|
163
|
+
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const { prompt, resolvedSkills } = await buildNodePromptWithResolvedSkillInstructions(spec, task, state.nodes, cwd);
|
|
167
|
+
node.resolvedSkills = resolvedSkills;
|
|
168
|
+
await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
|
|
169
|
+
const model = resolveModelForTask(task, spec.executorModels);
|
|
170
|
+
const started = Date.now();
|
|
171
|
+
try {
|
|
172
|
+
validateRepairArtifactGateBeforeShell({
|
|
173
|
+
task,
|
|
174
|
+
tasksById,
|
|
175
|
+
state,
|
|
176
|
+
});
|
|
177
|
+
const result = await executeNode({ task, cwd, model, prompt });
|
|
178
|
+
node.durationMs = result.durationMs ?? Date.now() - started;
|
|
179
|
+
node.stdout = result.stdout;
|
|
180
|
+
node.stderr = result.stderr;
|
|
181
|
+
node.failureCategory = result.failureCategory;
|
|
182
|
+
if (result.assistantText !== undefined) {
|
|
183
|
+
node.assistantText = result.assistantText;
|
|
184
|
+
}
|
|
185
|
+
if (result.backend !== undefined)
|
|
186
|
+
node.backend = result.backend;
|
|
187
|
+
if (result.sdkAttempted !== undefined) {
|
|
188
|
+
node.sdkAttempted = result.sdkAttempted;
|
|
189
|
+
}
|
|
190
|
+
if (result.tokensUsed !== undefined)
|
|
191
|
+
node.tokensUsed = result.tokensUsed;
|
|
192
|
+
if (result.parsedEvents !== undefined) {
|
|
193
|
+
node.parsedEvents = result.parsedEvents;
|
|
194
|
+
}
|
|
195
|
+
recordRepairArtifactForSupervisorNode({
|
|
196
|
+
task,
|
|
197
|
+
node,
|
|
198
|
+
state,
|
|
199
|
+
});
|
|
200
|
+
const outputChunk = result.assistantText ?? result.stdout;
|
|
201
|
+
if (outputChunk.trim()) {
|
|
202
|
+
await notifyNodeObserver(input.observer, "onNodeOutput", nodeId, state, outputChunk);
|
|
203
|
+
}
|
|
204
|
+
node.finishedAt = new Date().toISOString();
|
|
205
|
+
node.status = result.ok ? "FINISHED" : "ERROR";
|
|
206
|
+
if (result.ok) {
|
|
207
|
+
const decisionRecord = await recordDecisionEnvelopeForNode({
|
|
208
|
+
task,
|
|
209
|
+
runDir,
|
|
210
|
+
nodeId,
|
|
211
|
+
assistantText: node.assistantText ?? result.assistantText,
|
|
212
|
+
});
|
|
213
|
+
if (decisionRecord) {
|
|
214
|
+
node.decisionEnvelope = decisionRecord.nodeRecord;
|
|
215
|
+
if (!decisionRecord.nodeRecord.parseOk) {
|
|
216
|
+
node.status = "ERROR";
|
|
217
|
+
node.failureCategory = "decision-envelope-invalid";
|
|
218
|
+
node.stderr = [
|
|
219
|
+
node.stderr,
|
|
220
|
+
...(decisionRecord.nodeRecord.errors ?? []),
|
|
221
|
+
]
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join("\n");
|
|
224
|
+
}
|
|
225
|
+
else if (shouldPauseOnHumanEscalation(task, decisionRecord.nodeRecord) &&
|
|
226
|
+
decisionRecord.envelope) {
|
|
227
|
+
const pausedAt = new Date().toISOString();
|
|
228
|
+
const nodeArtifactsDir = path.join(runDir, nodeId);
|
|
229
|
+
const { jsonPath } = await writeHumanEscalationArtifacts({
|
|
230
|
+
nodeArtifactsDir,
|
|
231
|
+
runId: state.runId,
|
|
232
|
+
nodeId,
|
|
233
|
+
envelope: decisionRecord.envelope,
|
|
234
|
+
pausedAt,
|
|
235
|
+
});
|
|
236
|
+
node.pauseReason = "decision-gate-requires-human";
|
|
237
|
+
node.escalationArtifactPath = jsonPath;
|
|
238
|
+
input.onPause(nodeId, pausedAt, node.pauseReason);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
node.status = "ERROR";
|
|
245
|
+
node.stderr = error instanceof Error ? error.message : String(error);
|
|
246
|
+
node.finishedAt = new Date().toISOString();
|
|
247
|
+
node.durationMs = Date.now() - started;
|
|
248
|
+
}
|
|
249
|
+
if (node.status === "FINISHED") {
|
|
250
|
+
const artifactMeta = await persistLongNodeOutputArtifacts({
|
|
251
|
+
runDir,
|
|
252
|
+
nodeId,
|
|
253
|
+
stdout: node.stdout,
|
|
254
|
+
assistantText: node.assistantText,
|
|
255
|
+
});
|
|
256
|
+
Object.assign(node, artifactMeta);
|
|
257
|
+
}
|
|
258
|
+
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
259
|
+
await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
|
|
260
|
+
await input.persistState();
|
|
261
|
+
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
262
|
+
}
|