@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.
Files changed (143) hide show
  1. package/AGENTS.md +62 -45
  2. package/CHANGELOG.md +60 -28
  3. package/README.md +160 -124
  4. package/bin/loop-agent.js +21 -21
  5. package/dist/adapters/index.js +3 -2
  6. package/dist/adapters/loop-agent.js +44 -2
  7. package/dist/application/dag/args.js +420 -0
  8. package/dist/application/dag/generate-task-dag.js +280 -0
  9. package/dist/application/dag/report-dag.js +14 -0
  10. package/dist/application/dag/run-dag.js +106 -0
  11. package/dist/application/dag/validate-dag.js +102 -0
  12. package/dist/application/loop/run-action.js +23 -0
  13. package/dist/cli/catalog.js +2 -237
  14. package/dist/cli/command-definitions.js +571 -0
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/program.js +65 -1
  17. package/dist/cli/router.js +13 -0
  18. package/dist/cli-governance/active-residue-check.js +38 -0
  19. package/dist/commands/dag-report.js +6 -107
  20. package/dist/commands/dag-run-task.js +8 -466
  21. package/dist/commands/dag-validate.js +7 -179
  22. package/dist/commands/examples.js +90 -0
  23. package/dist/commands/init.js +1518 -0
  24. package/dist/commands/loop.js +57 -31
  25. package/dist/commands/pi-prompt.js +2 -9
  26. package/dist/commands/run-dag.js +7 -180
  27. package/dist/executors/cursor-executor-artifacts.js +3 -4
  28. package/dist/executors/cursor-worker-client.js +13 -3
  29. package/dist/executors/dag-cursor-executor.js +2 -3
  30. package/dist/executors/dag-pi-executor.js +3 -4
  31. package/dist/executors/dag-static-executor.js +2 -5
  32. package/dist/executors/pi-defaults.js +9 -0
  33. package/dist/executors/shell-executor.js +12 -20
  34. package/dist/governance/manifest-types.js +1 -0
  35. package/dist/infrastructure/harness/active-residue-policy.js +73 -0
  36. package/dist/infrastructure/harness/artifact-store.js +72 -0
  37. package/dist/infrastructure/harness/atomic-write.js +49 -0
  38. package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
  39. package/dist/infrastructure/harness/loop-action-store.js +23 -0
  40. package/dist/infrastructure/harness/loop-store.js +41 -0
  41. package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
  42. package/dist/infrastructure/harness/task-store.js +77 -0
  43. package/dist/records/one-shot-runs.js +26 -61
  44. package/dist/records/promotion.js +3 -4
  45. package/dist/shared/artifacts-core.js +5 -5
  46. package/dist/shared/logger.js +9 -15
  47. package/dist/task/delegate.js +4 -4
  48. package/dist/task/runtime.js +5 -7
  49. package/dist/task/state.js +6 -20
  50. package/dist/workflows/dag/convergence/controller.js +277 -0
  51. package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
  52. package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
  53. package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
  54. package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
  55. package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
  56. package/dist/workflows/dag/failure-routing.js +82 -0
  57. package/dist/workflows/dag/lifecycle.js +101 -8
  58. package/dist/workflows/dag/node-execution.js +262 -0
  59. package/dist/workflows/dag/report.js +73 -1
  60. package/dist/workflows/dag/run-store.js +36 -0
  61. package/dist/workflows/dag/runner.js +82 -1341
  62. package/dist/workflows/dag/scheduler.js +84 -0
  63. package/dist/workflows/dag/upstream-artifacts.js +20 -18
  64. package/dist/workflows/loop/actions/cursor-fix.js +191 -0
  65. package/dist/workflows/loop/actions/dag-action.js +130 -0
  66. package/dist/workflows/loop/actions/pi-review.js +267 -0
  67. package/dist/workflows/loop/actions/shared.js +157 -0
  68. package/dist/workflows/loop/actions/shell-verify.js +82 -0
  69. package/dist/workflows/loop/actions/types.js +1 -0
  70. package/dist/workflows/loop/actions/workflow-action.js +255 -0
  71. package/dist/workflows/loop/actions.js +55 -1212
  72. package/dist/workflows/loop/closeout.js +5 -4
  73. package/dist/workflows/loop/context.js +2 -3
  74. package/dist/workflows/loop/events.js +3 -2
  75. package/dist/workflows/loop/policy/auto-policy.js +104 -0
  76. package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
  77. package/dist/workflows/loop/rounds.js +3 -3
  78. package/dist/workflows/loop/signals.js +4 -7
  79. package/dist/workflows/loop/state.js +11 -11
  80. package/docs/README.md +47 -44
  81. package/docs/agent-dag-recovery-playbook.md +32 -6
  82. package/docs/agent-dag-runner.md +17 -17
  83. package/docs/architecture/runtime-boundaries.md +147 -0
  84. package/docs/cursor-executor-usage.md +5 -5
  85. package/docs/decisions/README.md +2 -2
  86. package/docs/design/README.md +24 -24
  87. package/docs/development-principles.md +50 -50
  88. package/docs/dynamic-workflow-dag-engine-roadmap.md +6 -6
  89. package/docs/exec-plans/README.md +4 -4
  90. package/docs/exec-plans/active/README.md +10 -5
  91. package/docs/exec-plans/completed/README.md +9 -5
  92. package/docs/feature-workflow.md +111 -109
  93. package/docs/harness-methodology-verification.md +18 -18
  94. package/docs/loop-agent-harness.md +36 -36
  95. package/docs/production-readiness.md +96 -0
  96. package/docs/progress/README.md +2 -2
  97. package/docs/reports/README.md +4 -2
  98. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
  99. package/docs/templates/agent-dag-process-supervisor.prompt.md +2 -2
  100. package/docs/templates/agent-dag-report.schema.json +33 -2
  101. package/docs/templates/agent-dag-review-verdict.prompt.md +1 -1
  102. package/docs/templates/agent-dag.base.json +195 -195
  103. package/docs/templates/agent-dag.final-verification.json +190 -190
  104. package/docs/templates/agent-dag.schema.json +17 -17
  105. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  106. package/docs/templates/hybrid-dag.json +193 -193
  107. package/docs/templates/production-readiness-checklist.md +57 -0
  108. package/docs/templates/progress-log.md +7 -7
  109. package/docs/templates/project-start-checklist.md +8 -8
  110. package/docs/templates/qa-report.md +17 -11
  111. package/docs/templates/sprint-contract.md +19 -19
  112. package/docs/verification-matrix.md +37 -26
  113. package/examples/example-dag.json +51 -51
  114. package/examples/hybrid-loop-agent-dag.json +194 -194
  115. package/harness.json +5 -5
  116. package/package.json +62 -61
  117. package/skills/ai-engineering-context/SKILL.md +21 -21
  118. package/skills/loop-agent/SKILL.md +56 -171
  119. package/skills/loop-agent/references/README.md +6 -2
  120. package/skills/loop-agent/references/command-reference.md +107 -65
  121. package/skills/loop-agent/references/harness-policy.md +115 -115
  122. package/skills/loop-agent/references/hybrid-dag.md +30 -30
  123. package/skills/loop-agent/references/learned/README.md +13 -13
  124. package/skills/loop-agent/references/long-running-loop.md +59 -0
  125. package/skills/loop-agent/references/model-routing.md +1 -1
  126. package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
  127. package/skills/loop-agent/references/pi-prompt.md +9 -9
  128. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
  129. package/skills/loop-agent/references/post-implementation-and-patterns.md +7 -7
  130. package/skills/loop-agent/references/task-workflow.md +19 -19
  131. package/skills/loop-agent/references/verification-and-failure-handling.md +54 -0
  132. package/skills/requesting-code-review/SKILL.md +40 -40
  133. package/skills/requesting-code-review/code-reviewer.md +4 -4
  134. package/skills/systematic-debugging/CREATION-LOG.md +43 -43
  135. package/skills/systematic-debugging/SKILL.md +113 -113
  136. package/skills/systematic-debugging/condition-based-waiting.md +20 -20
  137. package/skills/systematic-debugging/defense-in-depth.md +27 -27
  138. package/skills/systematic-debugging/root-cause-tracing.md +38 -38
  139. package/skills/systematic-debugging/test-academic.md +6 -6
  140. package/skills/systematic-debugging/test-pressure-1.md +6 -6
  141. package/skills/systematic-debugging/test-pressure-2.md +2 -2
  142. package/skills/systematic-debugging/test-pressure-3.md +6 -6
  143. 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
+ }