@tea-agent/loop-agent 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +43 -24
- package/CHANGELOG.md +72 -44
- package/README.md +177 -164
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/args.js +6 -0
- package/dist/application/dag/generate-task-dag.js +5 -3
- package/dist/application/dag/run-dag.js +17 -1
- package/dist/application/dag/validate-dag.js +41 -0
- package/dist/cli/command-definitions.js +2 -2
- package/dist/cli/program.js +24 -4
- package/dist/commands/init.js +1036 -461
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +2 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/failure-routing.js +82 -0
- package/dist/workflows/dag/init-hybrid.js +3 -3
- package/dist/workflows/dag/lifecycle.js +95 -3
- package/dist/workflows/dag/report.js +73 -1
- package/dist/workflows/dag/skills.js +3 -3
- package/dist/workflows/dag/types.js +2 -0
- package/dist/workflows/dynamic/compile.js +11 -0
- package/dist/workflows/dynamic/spec.js +1 -0
- package/docs/README.md +50 -45
- package/docs/agent-dag-recovery-playbook.md +32 -6
- package/docs/agent-dag-runner.md +19 -17
- package/docs/architecture/runtime-boundaries.md +1 -1
- 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 +7 -9
- package/docs/exec-plans/completed/README.md +10 -8
- package/docs/feature-workflow.md +111 -109
- package/docs/harness-methodology-verification.md +18 -18
- package/docs/init-surface.manifest.json +175 -0
- 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/skills/README.md +6 -0
- package/docs/skills/vetted-skill-registry.md +26 -0
- 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/init-evolution-review.md +33 -0
- 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 +10 -8
- package/package.json +60 -58
- package/skills/code-review-core/SKILL.md +20 -0
- package/skills/codebase-scout/SKILL.md +19 -0
- package/skills/init-capability-evolution/SKILL.md +69 -0
- package/skills/loop-agent/SKILL.md +35 -35
- package/skills/loop-agent/references/command-reference.md +125 -65
- package/skills/loop-agent/references/harness-policy.md +30 -30
- package/skills/loop-agent/references/hybrid-dag.md +30 -30
- 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/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 +36 -0
- package/skills/test-driven-development/SKILL.md +20 -0
- package/skills/webapp-testing/SKILL.md +19 -0
|
@@ -27,7 +27,7 @@ function renderLoopTemplate(template, iteration) {
|
|
|
27
27
|
.replace(/\{\{\s*iteration\s*\}\}/g, String(iteration))
|
|
28
28
|
.replace(/\{\{\s*iterationStatus\s*\}\}/g, iterationStatus);
|
|
29
29
|
}
|
|
30
|
-
function buildLoopBodyChildTask(input) {
|
|
30
|
+
export function buildLoopBodyChildTask(input) {
|
|
31
31
|
const { parent, bodyTask, iteration, nodeId, bodyIdMap } = input;
|
|
32
32
|
const mappedDepends = bodyTask.dependsOn.map((depId) => bodyIdMap.get(depId) ?? depId);
|
|
33
33
|
return {
|
|
@@ -37,6 +37,7 @@ function buildLoopBodyChildTask(input) {
|
|
|
37
37
|
subtask_prompt: renderLoopTemplate(bodyTask.subtaskPromptTemplate, iteration),
|
|
38
38
|
executor: bodyTask.executor,
|
|
39
39
|
role: bodyTask.role,
|
|
40
|
+
skills: bodyTask.skills,
|
|
40
41
|
writePolicy: bodyTask.writePolicy,
|
|
41
42
|
allowedPaths: bodyTask.allowedPaths,
|
|
42
43
|
forbiddenPaths: bodyTask.forbiddenPaths,
|
|
@@ -18,6 +18,7 @@ export function buildExpandedChildTask(input) {
|
|
|
18
18
|
subtask_prompt: subtaskPrompt,
|
|
19
19
|
executor: child.executor,
|
|
20
20
|
role: child.role,
|
|
21
|
+
skills: child.skills,
|
|
21
22
|
writePolicy: child.writePolicy,
|
|
22
23
|
allowedPaths: renderDynamicPatternList(child.allowedPaths, item, index, expansion.itemName) ?? [],
|
|
23
24
|
forbiddenPaths: renderDynamicPatternList(child.forbiddenPaths, item, index, expansion.itemName) ?? [],
|
|
@@ -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
|
+
}
|
|
@@ -22,9 +22,9 @@ const HYBRID_DEFAULTS = {
|
|
|
22
22
|
};
|
|
23
23
|
const HYBRID_SKILLS_BY_ROLE = {
|
|
24
24
|
planner: ["loop-agent"],
|
|
25
|
-
scout: ["ai-engineering-context"],
|
|
26
|
-
implementer: ["verification-before-completion"],
|
|
27
|
-
reviewer: ["requesting-code-review"],
|
|
25
|
+
scout: ["ai-engineering-context", "codebase-scout"],
|
|
26
|
+
implementer: ["test-driven-development", "verification-before-completion"],
|
|
27
|
+
reviewer: ["requesting-code-review", "code-review-core"],
|
|
28
28
|
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
29
29
|
closeout: ["loop-agent", "verification-before-completion"],
|
|
30
30
|
};
|
|
@@ -2,6 +2,8 @@ import { access, mkdir, readFile, readdir, rename, } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
4
4
|
import { parseDagSpec } from "./types.js";
|
|
5
|
+
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
6
|
+
import { routeDagFailure } from "./failure-routing.js";
|
|
5
7
|
const DAG_LIFECYCLE_SCAN_ORDER = [
|
|
6
8
|
"paused",
|
|
7
9
|
"active",
|
|
@@ -470,16 +472,106 @@ export async function runDagStatus(repoRoot, rawArgs) {
|
|
|
470
472
|
console.log(JSON.stringify(report, null, 2));
|
|
471
473
|
}
|
|
472
474
|
export function parseDagDoctorArgs(args) {
|
|
473
|
-
|
|
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
|
+
}
|
|
474
496
|
if (arg.startsWith("-")) {
|
|
475
497
|
throw new Error(`unknown dag doctor flag: ${arg}`);
|
|
476
498
|
}
|
|
477
499
|
throw new Error(`unexpected positional argument: ${arg}`);
|
|
478
500
|
}
|
|
479
|
-
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");
|
|
480
564
|
}
|
|
481
565
|
export async function runDagDoctor(repoRoot, rawArgs) {
|
|
482
|
-
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
|
+
}
|
|
483
575
|
const report = await buildDagDoctorReport(repoRoot);
|
|
484
576
|
console.log(JSON.stringify(report, null, 2));
|
|
485
577
|
}
|
|
@@ -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
|
"",
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const DEFAULT_SKILLS_BY_ROLE = {
|
|
6
6
|
planner: ["loop-agent"],
|
|
7
|
-
scout: ["ai-engineering-context"],
|
|
8
|
-
implementer: ["verification-before-completion"],
|
|
7
|
+
scout: ["ai-engineering-context", "codebase-scout"],
|
|
8
|
+
implementer: ["test-driven-development", "verification-before-completion"],
|
|
9
9
|
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
10
|
-
reviewer: ["requesting-code-review"],
|
|
10
|
+
reviewer: ["requesting-code-review", "code-review-core"],
|
|
11
11
|
supervisor: ["verification-before-completion", "loop-agent"],
|
|
12
12
|
closeout: ["loop-agent", "verification-before-completion"],
|
|
13
13
|
};
|
|
@@ -94,6 +94,7 @@ export const dagStaticConfigSchema = z.object({
|
|
|
94
94
|
export const dagDynamicExpansionChildTaskSchema = z.object({
|
|
95
95
|
executor: dagNodeExecutorSchema.default("pi"),
|
|
96
96
|
role: dagRoleSchema.optional(),
|
|
97
|
+
skills: z.array(z.string()).optional(),
|
|
97
98
|
complexity: dagComplexitySchema.default("LOW"),
|
|
98
99
|
subtaskPromptTemplate: z.string().min(1),
|
|
99
100
|
outputContract: z.string().optional(),
|
|
@@ -153,6 +154,7 @@ export const dagDynamicLoopBodyTaskSchema = z.object({
|
|
|
153
154
|
dependsOn: z.array(z.string()).default([]),
|
|
154
155
|
executor: dagNodeExecutorSchema.default("pi"),
|
|
155
156
|
role: dagRoleSchema.optional(),
|
|
157
|
+
skills: z.array(z.string()).optional(),
|
|
156
158
|
complexity: dagComplexitySchema.default("LOW"),
|
|
157
159
|
subtaskPromptTemplate: z.string().min(1),
|
|
158
160
|
outputContract: z.string().optional(),
|
|
@@ -40,6 +40,9 @@ function dagRoleForNode(node) {
|
|
|
40
40
|
function executorForAgentLikeNode(node) {
|
|
41
41
|
return node.executor ?? "pi";
|
|
42
42
|
}
|
|
43
|
+
function nodeSkillsField(node) {
|
|
44
|
+
return node.skills && node.skills.length > 0 ? { skills: node.skills } : {};
|
|
45
|
+
}
|
|
43
46
|
function dagWriteFields(spec, node) {
|
|
44
47
|
if (node?.writePolicy === "none") {
|
|
45
48
|
return {
|
|
@@ -111,6 +114,7 @@ function compileHumanGateNode(spec, node) {
|
|
|
111
114
|
executor: "pi",
|
|
112
115
|
role: "supervisor",
|
|
113
116
|
...dagWriteFields(spec, node),
|
|
117
|
+
...nodeSkillsField(node),
|
|
114
118
|
decisionGate: { enabled: true, mode: "pause-on-human" },
|
|
115
119
|
outputContract: node.approvalSchema
|
|
116
120
|
? `Workflow approvalSchema:\n${JSON.stringify(node.approvalSchema, null, 2)}`
|
|
@@ -172,6 +176,7 @@ function compileLoopBodyTask(spec, node) {
|
|
|
172
176
|
dependsOn: node.dependsOn,
|
|
173
177
|
executor,
|
|
174
178
|
role: dagRoleForNode(node),
|
|
179
|
+
...nodeSkillsField(node),
|
|
175
180
|
complexity: node.type === "reduce_agent" ? "MED" : "LOW",
|
|
176
181
|
subtaskPromptTemplate: buildAgentPrompt(node),
|
|
177
182
|
staticResultTemplate: node.staticResultTemplate ?? node.transform?.template ?? undefined,
|
|
@@ -189,6 +194,7 @@ function compileLoopUntilNode(spec, node) {
|
|
|
189
194
|
executor: "static",
|
|
190
195
|
role: dagRoleForNode(node),
|
|
191
196
|
...dagWriteFields(spec, node),
|
|
197
|
+
...nodeSkillsField(node),
|
|
192
198
|
static: {
|
|
193
199
|
resultMarkdown: `Dynamic loop_until barrier for ${node.id}.`,
|
|
194
200
|
},
|
|
@@ -212,6 +218,7 @@ function compileAgentLikeNode(spec, node) {
|
|
|
212
218
|
executor: "static",
|
|
213
219
|
role: dagRoleForNode(node),
|
|
214
220
|
...dagWriteFields(spec, node),
|
|
221
|
+
...nodeSkillsField(node),
|
|
215
222
|
static: {
|
|
216
223
|
resultMarkdown: node.staticResultTemplate ?? `Workflow node ${node.id} completed.`,
|
|
217
224
|
},
|
|
@@ -226,6 +233,7 @@ function compileAgentLikeNode(spec, node) {
|
|
|
226
233
|
executor: "pi",
|
|
227
234
|
role: dagRoleForNode(node),
|
|
228
235
|
...dagWriteFields(spec, node),
|
|
236
|
+
...nodeSkillsField(node),
|
|
229
237
|
outputContract: stringifyOutputSchema(node.outputSchema),
|
|
230
238
|
};
|
|
231
239
|
}
|
|
@@ -258,6 +266,7 @@ function compileDynamicExpansionNode(spec, node) {
|
|
|
258
266
|
const childTask = {
|
|
259
267
|
executor: childExecutor,
|
|
260
268
|
role: dagRoleForNode(node),
|
|
269
|
+
...nodeSkillsField(node),
|
|
261
270
|
complexity: "LOW",
|
|
262
271
|
subtaskPromptTemplate: childPrompt,
|
|
263
272
|
staticResultTemplate: node.staticResultTemplate,
|
|
@@ -273,6 +282,7 @@ function compileDynamicExpansionNode(spec, node) {
|
|
|
273
282
|
writePolicy: "none",
|
|
274
283
|
allowedPaths: [],
|
|
275
284
|
forbiddenPaths: [],
|
|
285
|
+
...nodeSkillsField(node),
|
|
276
286
|
static: {
|
|
277
287
|
resultMarkdown: `Dynamic ${node.type} expansion barrier for ${node.id}.`,
|
|
278
288
|
},
|
|
@@ -299,6 +309,7 @@ function compileDynamicReductionNode(spec, node) {
|
|
|
299
309
|
executor: "static",
|
|
300
310
|
role: dagRoleForNode(node),
|
|
301
311
|
...dagWriteFields(spec, node),
|
|
312
|
+
...nodeSkillsField(node),
|
|
302
313
|
static: {
|
|
303
314
|
resultMarkdown: `Dynamic reducer ${node.reducer?.type ?? "unknown"} for ${node.id}.`,
|
|
304
315
|
},
|
|
@@ -46,6 +46,7 @@ export const workflowNodeSchema = z.object({
|
|
|
46
46
|
dependsOn: z.array(z.string()).default([]),
|
|
47
47
|
executor: z.enum(["pi", "static"]).optional(),
|
|
48
48
|
role: z.string().optional(),
|
|
49
|
+
skills: z.array(z.string().min(1)).optional(),
|
|
49
50
|
prompt: z.string().optional(),
|
|
50
51
|
staticResultTemplate: z.string().optional(),
|
|
51
52
|
writePolicy: z.enum(["none", "read-only", "exclusive"]).optional(),
|