@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,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
|
+
}
|
|
@@ -7,6 +7,7 @@ import { repairArtifactSchema } from "./repair-artifact.js";
|
|
|
7
7
|
import { dagRunDirExists, getDagRunDir, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
8
8
|
export const DAG_CLOSEOUT_DRAFT_DISCLAIMER = "> **Advisory only.** Derived from completed run facts. Canonical source remains `dag report --json` and `.harness/dag-runs/completed/<run-id>/`. Do not treat this draft as authoritative.";
|
|
9
9
|
import { dagNormalizedFailureCategorySchema, normalizeDagFailureCategory, } from "./failure-category.js";
|
|
10
|
+
import { dagProductLineFailureCategoryValues, routeDagFailure, } from "./failure-routing.js";
|
|
10
11
|
import { DAG_RECOVERY_ACTIONS, planDagRecovery, } from "./recovery-recommendation.js";
|
|
11
12
|
import { dagNodeExecutorSchema, dagNodeStatusSchema, LEGACY_TOP_LEVEL_MODELS_ERROR, parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
12
13
|
export const DAG_REPORT_SCHEMA_VERSION = 1;
|
|
@@ -21,6 +22,7 @@ const dagRunStatusSchema = z.enum([
|
|
|
21
22
|
"paused",
|
|
22
23
|
]);
|
|
23
24
|
const dagRecoveryActionSchema = z.enum(DAG_RECOVERY_ACTIONS);
|
|
25
|
+
const dagProductLineFailureCategorySchema = z.enum(dagProductLineFailureCategoryValues);
|
|
24
26
|
const dagArtifactRefSchema = z
|
|
25
27
|
.object({
|
|
26
28
|
path: z.string(),
|
|
@@ -44,6 +46,8 @@ const dagReportPrimaryFailureSchema = z
|
|
|
44
46
|
nodeStatus: dagNodeStatusSchema.optional(),
|
|
45
47
|
failureCategory: z.string().optional(),
|
|
46
48
|
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
49
|
+
productLineFailureCategory: dagProductLineFailureCategorySchema.optional(),
|
|
50
|
+
recommendedFollowUp: z.string().optional(),
|
|
47
51
|
})
|
|
48
52
|
.strict();
|
|
49
53
|
const dagReportDownstreamSkippedNodeSchema = z
|
|
@@ -51,6 +55,8 @@ const dagReportDownstreamSkippedNodeSchema = z
|
|
|
51
55
|
nodeId: z.string(),
|
|
52
56
|
failureCategory: z.string().optional(),
|
|
53
57
|
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
58
|
+
productLineFailureCategory: dagProductLineFailureCategorySchema.optional(),
|
|
59
|
+
recommendedFollowUp: z.string().optional(),
|
|
54
60
|
})
|
|
55
61
|
.strict();
|
|
56
62
|
const dagConvergencePassArtifactRefSchema = z
|
|
@@ -130,6 +136,8 @@ const dagNodeReportRowSchema = z
|
|
|
130
136
|
tokensUsed: z.number().nonnegative().optional(),
|
|
131
137
|
failureCategory: z.string().optional(),
|
|
132
138
|
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
139
|
+
productLineFailureCategory: dagProductLineFailureCategorySchema.optional(),
|
|
140
|
+
recommendedFollowUp: z.string().optional(),
|
|
133
141
|
recoveryRecommendation: dagRecoveryRecommendationSchema.optional(),
|
|
134
142
|
backend: z.enum(["sdk", "cli"]).optional(),
|
|
135
143
|
sdkAttempted: z.boolean().optional(),
|
|
@@ -150,6 +158,8 @@ const dagRunReportEntrySchema = z
|
|
|
150
158
|
finishedAt: z.string().optional(),
|
|
151
159
|
failureCategory: z.string().optional(),
|
|
152
160
|
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
161
|
+
productLineFailureCategory: dagProductLineFailureCategorySchema.optional(),
|
|
162
|
+
recommendedFollowUp: z.string().optional(),
|
|
153
163
|
recoveryRecommendation: dagRecoveryRecommendationSchema.optional(),
|
|
154
164
|
primaryFailure: dagReportPrimaryFailureSchema,
|
|
155
165
|
primaryRecovery: dagRecoveryRecommendationSchema,
|
|
@@ -250,12 +260,18 @@ function buildPrimaryFailure(run, primaryNode) {
|
|
|
250
260
|
nodeStatus: primaryNode.status,
|
|
251
261
|
failureCategory: primaryNode.failureCategory,
|
|
252
262
|
normalizedFailureCategory: primaryNode.normalizedFailureCategory,
|
|
263
|
+
productLineFailureCategory: primaryNode.productLineFailureCategory,
|
|
264
|
+
recommendedFollowUp: primaryNode.recommendedFollowUp,
|
|
253
265
|
};
|
|
254
266
|
}
|
|
255
267
|
return {
|
|
256
268
|
scope: "run",
|
|
257
269
|
failureCategory: run.failureCategory,
|
|
258
270
|
normalizedFailureCategory: run.normalizedFailureCategory,
|
|
271
|
+
...routeDagFailure({
|
|
272
|
+
rawFailureCategory: run.failureCategory,
|
|
273
|
+
normalizedFailureCategory: run.normalizedFailureCategory,
|
|
274
|
+
}),
|
|
259
275
|
};
|
|
260
276
|
}
|
|
261
277
|
function resolvePrimaryRecovery(run, primaryNode) {
|
|
@@ -302,6 +318,8 @@ function collectDownstreamSkippedNodes(nodes, spec, primaryNode) {
|
|
|
302
318
|
nodeId: node.nodeId,
|
|
303
319
|
failureCategory: node.failureCategory,
|
|
304
320
|
normalizedFailureCategory: node.normalizedFailureCategory,
|
|
321
|
+
productLineFailureCategory: node.productLineFailureCategory,
|
|
322
|
+
recommendedFollowUp: node.recommendedFollowUp,
|
|
305
323
|
}));
|
|
306
324
|
}
|
|
307
325
|
function isFailedReportRun(run) {
|
|
@@ -342,6 +360,12 @@ export async function buildDagRunReportEntry(input) {
|
|
|
342
360
|
continue;
|
|
343
361
|
const task = tasks.get(nodeId);
|
|
344
362
|
const normalizedFailureCategory = normalizeDagFailureCategory(node.failureCategory, node.status);
|
|
363
|
+
const failureRouting = routeDagFailure({
|
|
364
|
+
rawFailureCategory: node.failureCategory,
|
|
365
|
+
normalizedFailureCategory,
|
|
366
|
+
nodeId,
|
|
367
|
+
executor: node.executor,
|
|
368
|
+
});
|
|
345
369
|
nodes.push({
|
|
346
370
|
nodeId,
|
|
347
371
|
rank,
|
|
@@ -355,6 +379,7 @@ export async function buildDagRunReportEntry(input) {
|
|
|
355
379
|
tokensUsed: node.tokensUsed,
|
|
356
380
|
failureCategory: node.failureCategory,
|
|
357
381
|
normalizedFailureCategory,
|
|
382
|
+
...failureRouting,
|
|
358
383
|
recoveryRecommendation: planDagRecovery({
|
|
359
384
|
status: node.status,
|
|
360
385
|
normalizedFailureCategory,
|
|
@@ -406,6 +431,11 @@ export async function buildDagRunReportEntry(input) {
|
|
|
406
431
|
lifecycle: input.lifecycle,
|
|
407
432
|
runStatus: input.state.status,
|
|
408
433
|
});
|
|
434
|
+
const runFailureRouting = routeDagFailure({
|
|
435
|
+
rawFailureCategory: runRecoverySource.failureCategory,
|
|
436
|
+
normalizedFailureCategory: runRecoverySource.normalizedFailureCategory,
|
|
437
|
+
nodeId: "nodeId" in runRecoverySource ? runRecoverySource.nodeId : undefined,
|
|
438
|
+
});
|
|
409
439
|
const partialEntry = {
|
|
410
440
|
runId: input.state.runId,
|
|
411
441
|
title: input.state.title,
|
|
@@ -416,6 +446,7 @@ export async function buildDagRunReportEntry(input) {
|
|
|
416
446
|
finishedAt: input.state.finishedAt,
|
|
417
447
|
failureCategory: input.state.failureCategory,
|
|
418
448
|
normalizedFailureCategory,
|
|
449
|
+
...runFailureRouting,
|
|
419
450
|
recoveryRecommendation,
|
|
420
451
|
pausedByNodeId: input.state.pausedByNodeId,
|
|
421
452
|
pauseReason: input.state.pauseReason,
|
|
@@ -638,11 +669,15 @@ function formatPrimaryFailureSection(run) {
|
|
|
638
669
|
return [
|
|
639
670
|
`- **Node**: ${failure.nodeId} (${failure.nodeStatus ?? "unknown"})`,
|
|
640
671
|
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
672
|
+
`- **Product-line category**: ${failure.productLineFailureCategory ?? "-"}`,
|
|
673
|
+
`- **Recommended follow-up**: ${failure.recommendedFollowUp ?? "-"}`,
|
|
641
674
|
];
|
|
642
675
|
}
|
|
643
676
|
return [
|
|
644
677
|
`- **Scope**: run`,
|
|
645
678
|
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
679
|
+
`- **Product-line category**: ${failure.productLineFailureCategory ?? "-"}`,
|
|
680
|
+
`- **Recommended follow-up**: ${failure.recommendedFollowUp ?? "-"}`,
|
|
646
681
|
];
|
|
647
682
|
}
|
|
648
683
|
function formatPrimaryRecoverySection(run) {
|
|
@@ -784,7 +819,7 @@ export function formatDagReportHandoffMarkdown(report) {
|
|
|
784
819
|
? `- **Failure (raw/normalized)**: ${node.failureCategory} / ${node.normalizedFailureCategory ?? "-"}`
|
|
785
820
|
: "", `- **Executor**: ${node.executor}${node.backend ? ` (backend: ${node.backend})` : ""}`, `- **Model**: ${node.model ?? "—"}`, `- **Duration**: ${formatDuration(node.durationMs)}`, `- **Tokens**: ${formatTokens(node.tokensUsed)}`, `- **Started**: ${formatTimestamp(node.startedAt)}`, `- **Finished**: ${formatTimestamp(node.finishedAt)}`, "");
|
|
786
821
|
}
|
|
787
|
-
lines.push("## Failures", ...formatFailureSection(run), "", "## Recovery Plan", ...formatRecoveryPlanSection(run), "", "## Artifacts", ...formatArtifactsSection(run), "", "## Suggested Next Action", `- ${suggestedNextAction(run)}`, "");
|
|
822
|
+
lines.push("## Failures", ...formatFailureSection(run), "", "## Recovery Plan", ...formatRecoveryPlanSection(run), "", "## Operator Next Steps", ...formatRecommendedOperatorAction(run), "", "## Artifacts", ...formatArtifactsSection(run), "", "## Suggested Next Action", `- ${suggestedNextAction(run)}`, "");
|
|
788
823
|
return lines
|
|
789
824
|
.filter((line) => line !== undefined)
|
|
790
825
|
.join("\n");
|
|
@@ -869,6 +904,8 @@ function formatCloseoutPrimaryFailureSection(run) {
|
|
|
869
904
|
`- **Scope**: node`,
|
|
870
905
|
`- **Node**: ${failure.nodeId} (${failure.nodeStatus ?? "unknown"})`,
|
|
871
906
|
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
907
|
+
`- **Product-line category**: ${failure.productLineFailureCategory ?? "-"}`,
|
|
908
|
+
`- **Recommended follow-up**: ${failure.recommendedFollowUp ?? "-"}`,
|
|
872
909
|
];
|
|
873
910
|
}
|
|
874
911
|
if (run.primaryRecovery.action === "none" &&
|
|
@@ -879,6 +916,8 @@ function formatCloseoutPrimaryFailureSection(run) {
|
|
|
879
916
|
return [
|
|
880
917
|
`- **Scope**: ${failure.scope}`,
|
|
881
918
|
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
919
|
+
`- **Product-line category**: ${failure.productLineFailureCategory ?? "-"}`,
|
|
920
|
+
`- **Recommended follow-up**: ${failure.recommendedFollowUp ?? "-"}`,
|
|
882
921
|
];
|
|
883
922
|
}
|
|
884
923
|
function formatCloseoutRecoverySection(run) {
|
|
@@ -939,6 +978,39 @@ function formatRemainingRisksSection(run) {
|
|
|
939
978
|
return risks;
|
|
940
979
|
}
|
|
941
980
|
export function formatDagCloseoutDraftMarkdown(run) {
|
|
981
|
+
if (isFailedReportRun(run)) {
|
|
982
|
+
const lines = [
|
|
983
|
+
`# Failure Handoff: ${run.runId}`,
|
|
984
|
+
"",
|
|
985
|
+
DAG_CLOSEOUT_DRAFT_DISCLAIMER,
|
|
986
|
+
"",
|
|
987
|
+
"## What failed",
|
|
988
|
+
...formatCloseoutPrimaryFailureSection(run),
|
|
989
|
+
"",
|
|
990
|
+
"## Evidence",
|
|
991
|
+
...formatCloseoutVerificationEvidence(run),
|
|
992
|
+
"",
|
|
993
|
+
"## Classification",
|
|
994
|
+
`- raw_failure_category: ${run.primaryFailure.failureCategory ?? "-"}`,
|
|
995
|
+
`- dag_normalized_failure_category: ${run.primaryFailure.normalizedFailureCategory ?? "-"}`,
|
|
996
|
+
`- product_line_failure_category: ${run.primaryFailure.productLineFailureCategory ?? "-"}`,
|
|
997
|
+
`- recommended_follow_up: ${run.primaryFailure.recommendedFollowUp ?? "-"}`,
|
|
998
|
+
"",
|
|
999
|
+
"## Recommended follow-up",
|
|
1000
|
+
...formatCloseoutRecoverySection(run),
|
|
1001
|
+
"",
|
|
1002
|
+
"## Safe retry conditions",
|
|
1003
|
+
`- Retry only after completing \`${run.primaryFailure.recommendedFollowUp ?? run.primaryRecovery.action}\` and preserving the original DAG run facts.`,
|
|
1004
|
+
"- Do not rewrite `.harness/dag-runs/completed/**`; create a new run or task artifact for follow-up evidence.",
|
|
1005
|
+
"",
|
|
1006
|
+
"## Human decision needed",
|
|
1007
|
+
run.primaryRecovery.humanRequired
|
|
1008
|
+
? "- Yes. Human review is required before retry or promotion."
|
|
1009
|
+
: "- No required human gate was derived, but review the failure evidence before retry.",
|
|
1010
|
+
"",
|
|
1011
|
+
];
|
|
1012
|
+
return lines.join("\n");
|
|
1013
|
+
}
|
|
942
1014
|
const lines = [
|
|
943
1015
|
`# DAG Closeout Draft: ${run.runId}`,
|
|
944
1016
|
"",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
4
|
+
import { readDagRunSpec, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
|
|
5
|
+
export async function readRunState(runDir) {
|
|
6
|
+
return readDagRunState(runDir);
|
|
7
|
+
}
|
|
8
|
+
export async function writeRunState(runDir, state, options) {
|
|
9
|
+
await writeDagRunState(runDir, state, options);
|
|
10
|
+
}
|
|
11
|
+
export async function readRunSpec(runDir) {
|
|
12
|
+
return readDagRunSpec(runDir);
|
|
13
|
+
}
|
|
14
|
+
export async function prepareActiveRunDir(runDir) {
|
|
15
|
+
await rm(runDir, { recursive: true, force: true });
|
|
16
|
+
await mkdir(runDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
export async function writeRunSpec(runDir, spec) {
|
|
19
|
+
await writeJsonAtomic(path.join(runDir, "run.json"), spec);
|
|
20
|
+
}
|
|
21
|
+
export async function writeNodeRecord(runDir, nodeId, record) {
|
|
22
|
+
await writeJsonAtomic(path.join(runDir, `${nodeId}.json`), record);
|
|
23
|
+
}
|
|
24
|
+
export async function writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills) {
|
|
25
|
+
if (resolvedSkills.length === 0)
|
|
26
|
+
return;
|
|
27
|
+
const nodeDir = path.join(runDir, nodeId);
|
|
28
|
+
await mkdir(nodeDir, { recursive: true });
|
|
29
|
+
await writeJsonAtomic(path.join(nodeDir, "skills.json"), { resolvedSkills });
|
|
30
|
+
}
|
|
31
|
+
export async function moveToPausedRunDir(runDir, pausedRunDir) {
|
|
32
|
+
return transferDagRunDir(runDir, pausedRunDir);
|
|
33
|
+
}
|
|
34
|
+
export async function moveToCompletedRunDir(runDir, completedRunDir) {
|
|
35
|
+
return transferDagRunDir(runDir, completedRunDir);
|
|
36
|
+
}
|