@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.
Files changed (77) hide show
  1. package/AGENTS.md +43 -24
  2. package/CHANGELOG.md +72 -44
  3. package/README.md +177 -164
  4. package/bin/loop-agent.js +21 -21
  5. package/dist/application/dag/args.js +6 -0
  6. package/dist/application/dag/generate-task-dag.js +5 -3
  7. package/dist/application/dag/run-dag.js +17 -1
  8. package/dist/application/dag/validate-dag.js +41 -0
  9. package/dist/cli/command-definitions.js +2 -2
  10. package/dist/cli/program.js +24 -4
  11. package/dist/commands/init.js +1036 -461
  12. package/dist/workflows/dag/dynamic-runtime/loop-until.js +2 -1
  13. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  14. package/dist/workflows/dag/failure-routing.js +82 -0
  15. package/dist/workflows/dag/init-hybrid.js +3 -3
  16. package/dist/workflows/dag/lifecycle.js +95 -3
  17. package/dist/workflows/dag/report.js +73 -1
  18. package/dist/workflows/dag/skills.js +3 -3
  19. package/dist/workflows/dag/types.js +2 -0
  20. package/dist/workflows/dynamic/compile.js +11 -0
  21. package/dist/workflows/dynamic/spec.js +1 -0
  22. package/docs/README.md +50 -45
  23. package/docs/agent-dag-recovery-playbook.md +32 -6
  24. package/docs/agent-dag-runner.md +19 -17
  25. package/docs/architecture/runtime-boundaries.md +1 -1
  26. package/docs/cursor-executor-usage.md +5 -5
  27. package/docs/decisions/README.md +2 -2
  28. package/docs/design/README.md +24 -24
  29. package/docs/development-principles.md +50 -50
  30. package/docs/dynamic-workflow-dag-engine-roadmap.md +6 -6
  31. package/docs/exec-plans/README.md +4 -4
  32. package/docs/exec-plans/active/README.md +7 -9
  33. package/docs/exec-plans/completed/README.md +10 -8
  34. package/docs/feature-workflow.md +111 -109
  35. package/docs/harness-methodology-verification.md +18 -18
  36. package/docs/init-surface.manifest.json +175 -0
  37. package/docs/loop-agent-harness.md +36 -36
  38. package/docs/production-readiness.md +96 -0
  39. package/docs/progress/README.md +2 -2
  40. package/docs/reports/README.md +4 -2
  41. package/docs/skills/README.md +6 -0
  42. package/docs/skills/vetted-skill-registry.md +26 -0
  43. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
  44. package/docs/templates/agent-dag-process-supervisor.prompt.md +2 -2
  45. package/docs/templates/agent-dag-report.schema.json +33 -2
  46. package/docs/templates/agent-dag-review-verdict.prompt.md +1 -1
  47. package/docs/templates/agent-dag.base.json +195 -195
  48. package/docs/templates/agent-dag.final-verification.json +190 -190
  49. package/docs/templates/agent-dag.schema.json +17 -17
  50. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  51. package/docs/templates/hybrid-dag.json +193 -193
  52. package/docs/templates/init-evolution-review.md +33 -0
  53. package/docs/templates/production-readiness-checklist.md +57 -0
  54. package/docs/templates/progress-log.md +7 -7
  55. package/docs/templates/project-start-checklist.md +8 -8
  56. package/docs/templates/qa-report.md +17 -11
  57. package/docs/templates/sprint-contract.md +19 -19
  58. package/docs/verification-matrix.md +37 -26
  59. package/examples/example-dag.json +51 -51
  60. package/examples/hybrid-loop-agent-dag.json +194 -194
  61. package/harness.json +10 -8
  62. package/package.json +60 -58
  63. package/skills/code-review-core/SKILL.md +20 -0
  64. package/skills/codebase-scout/SKILL.md +19 -0
  65. package/skills/init-capability-evolution/SKILL.md +69 -0
  66. package/skills/loop-agent/SKILL.md +35 -35
  67. package/skills/loop-agent/references/command-reference.md +125 -65
  68. package/skills/loop-agent/references/harness-policy.md +30 -30
  69. package/skills/loop-agent/references/hybrid-dag.md +30 -30
  70. package/skills/loop-agent/references/model-routing.md +1 -1
  71. package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
  72. package/skills/loop-agent/references/pi-prompt.md +9 -9
  73. package/skills/loop-agent/references/post-implementation-and-patterns.md +7 -7
  74. package/skills/loop-agent/references/task-workflow.md +19 -19
  75. package/skills/loop-agent/references/verification-and-failure-handling.md +36 -0
  76. package/skills/test-driven-development/SKILL.md +20 -0
  77. 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
- for (const arg of args) {
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(),