@tea-agent/loop-agent 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +62 -45
- package/CHANGELOG.md +60 -28
- package/README.md +160 -124
- package/bin/loop-agent.js +21 -21
- 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 +106 -0
- package/dist/application/dag/validate-dag.js +102 -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 +1518 -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/failure-routing.js +82 -0
- package/dist/workflows/dag/lifecycle.js +101 -8
- package/dist/workflows/dag/node-execution.js +262 -0
- package/dist/workflows/dag/report.js +73 -1
- 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 +47 -44
- package/docs/agent-dag-recovery-playbook.md +32 -6
- package/docs/agent-dag-runner.md +17 -17
- package/docs/architecture/runtime-boundaries.md +147 -0
- package/docs/cursor-executor-usage.md +5 -5
- package/docs/decisions/README.md +2 -2
- package/docs/design/README.md +24 -24
- package/docs/development-principles.md +50 -50
- package/docs/dynamic-workflow-dag-engine-roadmap.md +6 -6
- package/docs/exec-plans/README.md +4 -4
- package/docs/exec-plans/active/README.md +10 -5
- package/docs/exec-plans/completed/README.md +9 -5
- package/docs/feature-workflow.md +111 -109
- package/docs/harness-methodology-verification.md +18 -18
- package/docs/loop-agent-harness.md +36 -36
- package/docs/production-readiness.md +96 -0
- package/docs/progress/README.md +2 -2
- package/docs/reports/README.md +4 -2
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
- package/docs/templates/agent-dag-process-supervisor.prompt.md +2 -2
- package/docs/templates/agent-dag-report.schema.json +33 -2
- package/docs/templates/agent-dag-review-verdict.prompt.md +1 -1
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +17 -17
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/production-readiness-checklist.md +57 -0
- package/docs/templates/progress-log.md +7 -7
- package/docs/templates/project-start-checklist.md +8 -8
- package/docs/templates/qa-report.md +17 -11
- package/docs/templates/sprint-contract.md +19 -19
- package/docs/verification-matrix.md +37 -26
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +5 -5
- package/package.json +62 -61
- package/skills/ai-engineering-context/SKILL.md +21 -21
- package/skills/loop-agent/SKILL.md +56 -171
- package/skills/loop-agent/references/README.md +6 -2
- package/skills/loop-agent/references/command-reference.md +107 -65
- package/skills/loop-agent/references/harness-policy.md +115 -115
- package/skills/loop-agent/references/hybrid-dag.md +30 -30
- 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/model-routing.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/pi-prompt.md +9 -9
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
- package/skills/loop-agent/references/post-implementation-and-patterns.md +7 -7
- package/skills/loop-agent/references/task-workflow.md +19 -19
- package/skills/loop-agent/references/verification-and-failure-handling.md +54 -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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const dagProductLineFailureCategoryValues = [
|
|
2
|
+
"SpecUnclear",
|
|
3
|
+
"ContractMismatch",
|
|
4
|
+
"ProductBug",
|
|
5
|
+
"TestBug",
|
|
6
|
+
"EnvFailure",
|
|
7
|
+
"FlakyTest",
|
|
8
|
+
"RiskyChange",
|
|
9
|
+
"DependencyFailure",
|
|
10
|
+
"NeedsHuman",
|
|
11
|
+
"Unknown",
|
|
12
|
+
];
|
|
13
|
+
const FOLLOW_UP_BY_PRODUCT_LINE = {
|
|
14
|
+
SpecUnclear: "spec-clarification",
|
|
15
|
+
ContractMismatch: "architecture-contract-fix",
|
|
16
|
+
ProductBug: "dev-fix",
|
|
17
|
+
TestBug: "qa-fix-test",
|
|
18
|
+
EnvFailure: "env-fix or retry verify",
|
|
19
|
+
FlakyTest: "flaky-test-analysis",
|
|
20
|
+
RiskyChange: "human-review or architecture-review",
|
|
21
|
+
DependencyFailure: "unblock dependency",
|
|
22
|
+
NeedsHuman: "human-review",
|
|
23
|
+
Unknown: "human triage",
|
|
24
|
+
};
|
|
25
|
+
function routeToProductLine(input) {
|
|
26
|
+
const normalized = input.normalizedFailureCategory;
|
|
27
|
+
if (!normalized || normalized === "success")
|
|
28
|
+
return undefined;
|
|
29
|
+
const raw = input.rawFailureCategory?.toLowerCase() ?? "";
|
|
30
|
+
const nodeId = input.nodeId?.toLowerCase() ?? "";
|
|
31
|
+
switch (normalized) {
|
|
32
|
+
case "write-guard":
|
|
33
|
+
return "RiskyChange";
|
|
34
|
+
case "auth":
|
|
35
|
+
case "executor":
|
|
36
|
+
case "timeout":
|
|
37
|
+
return "EnvFailure";
|
|
38
|
+
case "human-required":
|
|
39
|
+
case "human-rejected":
|
|
40
|
+
case "decision-envelope":
|
|
41
|
+
return "NeedsHuman";
|
|
42
|
+
case "skipped":
|
|
43
|
+
return "DependencyFailure";
|
|
44
|
+
case "shell-command":
|
|
45
|
+
if (raw.includes("flaky"))
|
|
46
|
+
return "FlakyTest";
|
|
47
|
+
if (nodeId.includes("test") || raw.includes("test-bug")) {
|
|
48
|
+
return "TestBug";
|
|
49
|
+
}
|
|
50
|
+
return "ProductBug";
|
|
51
|
+
case "static-error":
|
|
52
|
+
return "SpecUnclear";
|
|
53
|
+
case "validation":
|
|
54
|
+
if (raw.includes("path") ||
|
|
55
|
+
raw.includes("write") ||
|
|
56
|
+
raw.includes("forbidden")) {
|
|
57
|
+
return "RiskyChange";
|
|
58
|
+
}
|
|
59
|
+
if (raw.includes("test-bug") || nodeId.includes("test")) {
|
|
60
|
+
return "TestBug";
|
|
61
|
+
}
|
|
62
|
+
if (raw.includes("test-failure") || raw.includes("verify-failure")) {
|
|
63
|
+
return "ProductBug";
|
|
64
|
+
}
|
|
65
|
+
return "SpecUnclear";
|
|
66
|
+
case "unknown":
|
|
67
|
+
return "Unknown";
|
|
68
|
+
default: {
|
|
69
|
+
const exhaustive = normalized;
|
|
70
|
+
return exhaustive;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function routeDagFailure(input) {
|
|
75
|
+
const productLineFailureCategory = routeToProductLine(input);
|
|
76
|
+
if (!productLineFailureCategory)
|
|
77
|
+
return {};
|
|
78
|
+
return {
|
|
79
|
+
productLineFailureCategory,
|
|
80
|
+
recommendedFollowUp: FOLLOW_UP_BY_PRODUCT_LINE[productLineFailureCategory],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
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";
|
|
5
|
+
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
6
|
+
import { routeDagFailure } from "./failure-routing.js";
|
|
4
7
|
const DAG_LIFECYCLE_SCAN_ORDER = [
|
|
5
8
|
"paused",
|
|
6
9
|
"active",
|
|
@@ -24,8 +27,8 @@ export async function readDagRunState(runDir) {
|
|
|
24
27
|
const raw = JSON.parse(await readFile(path.join(runDir, "state.json"), "utf-8"));
|
|
25
28
|
return raw;
|
|
26
29
|
}
|
|
27
|
-
export async function writeDagRunState(runDir, state) {
|
|
28
|
-
await
|
|
30
|
+
export async function writeDagRunState(runDir, state, options) {
|
|
31
|
+
await writeJsonAtomic(path.join(runDir, "state.json"), state, options);
|
|
29
32
|
}
|
|
30
33
|
export async function readDagRunSpec(runDir) {
|
|
31
34
|
const raw = JSON.parse(await readFile(path.join(runDir, "run.json"), "utf-8"));
|
|
@@ -131,14 +134,14 @@ export async function writeHumanApprovalArtifact(input) {
|
|
|
131
134
|
const nodeDir = path.join(input.runDir, input.nodeId);
|
|
132
135
|
await mkdir(nodeDir, { recursive: true });
|
|
133
136
|
const artifactPath = path.join(nodeDir, "human-approval.json");
|
|
134
|
-
await
|
|
137
|
+
await writeJsonAtomic(artifactPath, input.artifact);
|
|
135
138
|
return artifactPath;
|
|
136
139
|
}
|
|
137
140
|
export async function writeHumanRejectionArtifact(input) {
|
|
138
141
|
const nodeDir = path.join(input.runDir, input.nodeId);
|
|
139
142
|
await mkdir(nodeDir, { recursive: true });
|
|
140
143
|
const artifactPath = path.join(nodeDir, "human-rejection.json");
|
|
141
|
-
await
|
|
144
|
+
await writeJsonAtomic(artifactPath, input.artifact);
|
|
142
145
|
return artifactPath;
|
|
143
146
|
}
|
|
144
147
|
export async function readHumanApprovalArtifact(runDir, nodeId) {
|
|
@@ -469,16 +472,106 @@ export async function runDagStatus(repoRoot, rawArgs) {
|
|
|
469
472
|
console.log(JSON.stringify(report, null, 2));
|
|
470
473
|
}
|
|
471
474
|
export function parseDagDoctorArgs(args) {
|
|
472
|
-
|
|
475
|
+
let runId;
|
|
476
|
+
let markdown = false;
|
|
477
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
478
|
+
const arg = args[i];
|
|
479
|
+
if (arg === "--run-id") {
|
|
480
|
+
runId = args[++i];
|
|
481
|
+
if (!runId || runId.startsWith("-")) {
|
|
482
|
+
throw new Error("dag doctor --run-id requires a value");
|
|
483
|
+
}
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (arg.startsWith("--run-id=")) {
|
|
487
|
+
runId = arg.slice("--run-id=".length);
|
|
488
|
+
if (!runId)
|
|
489
|
+
throw new Error("dag doctor --run-id requires a value");
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (arg === "--markdown") {
|
|
493
|
+
markdown = true;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
473
496
|
if (arg.startsWith("-")) {
|
|
474
497
|
throw new Error(`unknown dag doctor flag: ${arg}`);
|
|
475
498
|
}
|
|
476
499
|
throw new Error(`unexpected positional argument: ${arg}`);
|
|
477
500
|
}
|
|
478
|
-
return {};
|
|
501
|
+
return { runId, markdown };
|
|
502
|
+
}
|
|
503
|
+
function findDoctorFailureNode(state) {
|
|
504
|
+
if (state.pausedByNodeId) {
|
|
505
|
+
const node = state.nodes[state.pausedByNodeId];
|
|
506
|
+
return {
|
|
507
|
+
nodeId: state.pausedByNodeId,
|
|
508
|
+
status: node?.status,
|
|
509
|
+
rawFailureCategory: node?.failureCategory,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const errorEntry = Object.entries(state.nodes).find(([, node]) => node.status === "ERROR");
|
|
513
|
+
const skippedEntry = Object.entries(state.nodes).find(([, node]) => node.status === "SKIPPED");
|
|
514
|
+
const selected = errorEntry ?? skippedEntry;
|
|
515
|
+
if (!selected) {
|
|
516
|
+
return {
|
|
517
|
+
rawFailureCategory: state.failureCategory,
|
|
518
|
+
status: state.status,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
nodeId: selected[0],
|
|
523
|
+
status: selected[1].status,
|
|
524
|
+
rawFailureCategory: selected[1].failureCategory,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
async function formatDagDoctorMarkdown(repoRoot, runId) {
|
|
528
|
+
const located = await locateDagRun(repoRoot, runId);
|
|
529
|
+
if (!located) {
|
|
530
|
+
throw new Error(`dag run not found: ${runId}`);
|
|
531
|
+
}
|
|
532
|
+
const state = await readDagRunState(located.runDir);
|
|
533
|
+
const summary = await buildDagStatusReport(repoRoot, runId);
|
|
534
|
+
const failure = findDoctorFailureNode(state);
|
|
535
|
+
const rawFailureCategory = failure.rawFailureCategory ??
|
|
536
|
+
(state.status === "paused" ? "human-required" : state.failureCategory);
|
|
537
|
+
const failureStatus = state.status === "paused" ? "paused" : failure.status;
|
|
538
|
+
const normalizedCategory = normalizeDagFailureCategory(rawFailureCategory, failureStatus ?? state.status);
|
|
539
|
+
const routing = routeDagFailure({
|
|
540
|
+
rawFailureCategory,
|
|
541
|
+
normalizedFailureCategory: normalizedCategory,
|
|
542
|
+
nodeId: failure.nodeId,
|
|
543
|
+
});
|
|
544
|
+
const evidence = failure.nodeId
|
|
545
|
+
? path.join(located.runDir, failure.nodeId, "result.summary.md")
|
|
546
|
+
: path.join(located.runDir, "state.json");
|
|
547
|
+
const nextCommand = summary.nextRecommendedAction ||
|
|
548
|
+
routing.recommendedFollowUp ||
|
|
549
|
+
"Inspect run facts and choose a recovery path.";
|
|
550
|
+
return [
|
|
551
|
+
"## Diagnosis",
|
|
552
|
+
"",
|
|
553
|
+
`- run id: ${runId}`,
|
|
554
|
+
`- lifecycle: ${located.lifecycle}`,
|
|
555
|
+
`- failed node: ${failure.nodeId ?? "-"}`,
|
|
556
|
+
`- raw failure: ${rawFailureCategory ?? "-"}`,
|
|
557
|
+
`- normalized category: ${normalizedCategory}`,
|
|
558
|
+
`- product-line category: ${routing.productLineFailureCategory ?? "-"}`,
|
|
559
|
+
`- recommended follow-up: ${routing.recommendedFollowUp ?? "-"}`,
|
|
560
|
+
`- evidence: ${evidence}`,
|
|
561
|
+
`- next command: ${nextCommand}`,
|
|
562
|
+
"",
|
|
563
|
+
].join("\n");
|
|
479
564
|
}
|
|
480
565
|
export async function runDagDoctor(repoRoot, rawArgs) {
|
|
481
|
-
parseDagDoctorArgs(rawArgs);
|
|
566
|
+
const parsed = parseDagDoctorArgs(rawArgs);
|
|
567
|
+
if (parsed.runId) {
|
|
568
|
+
if (parsed.markdown) {
|
|
569
|
+
console.log(await formatDagDoctorMarkdown(repoRoot, parsed.runId));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
console.log(JSON.stringify(await buildDagStatusReport(repoRoot, parsed.runId), null, 2));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
482
575
|
const report = await buildDagDoctorReport(repoRoot);
|
|
483
576
|
console.log(JSON.stringify(report, null, 2));
|
|
484
577
|
}
|