@tea-agent/loop-agent 0.1.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 +121 -0
- package/CHANGELOG.md +37 -0
- package/README.md +144 -0
- package/bin/loop-agent.js +21 -0
- package/dist/adapters/aimax.js +91 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/loop-agent.js +98 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/catalog.js +259 -0
- package/dist/cli/help.js +55 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.js +505 -0
- package/dist/cli.js +12 -0
- package/dist/commands/closeout.js +13 -0
- package/dist/commands/coverage-audit.js +14 -0
- package/dist/commands/cursor-prompt.js +222 -0
- package/dist/commands/cursor-worker.js +43 -0
- package/dist/commands/dag-approve.js +102 -0
- package/dist/commands/dag-final-verification.js +76 -0
- package/dist/commands/dag-init-hybrid.js +56 -0
- package/dist/commands/dag-reconcile-tasks.js +51 -0
- package/dist/commands/dag-reject.js +91 -0
- package/dist/commands/dag-report.js +177 -0
- package/dist/commands/dag-resume.js +34 -0
- package/dist/commands/dag-run-task.js +470 -0
- package/dist/commands/dag-validate.js +186 -0
- package/dist/commands/dag-workflow-compile.js +91 -0
- package/dist/commands/dag-workflow-plan.js +130 -0
- package/dist/commands/dag-workflow-validate.js +66 -0
- package/dist/commands/delegate.js +132 -0
- package/dist/commands/docs-archive.js +5 -0
- package/dist/commands/docs-audit.js +5 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/goal.js +92 -0
- package/dist/commands/handoff-check.js +5 -0
- package/dist/commands/harvest.js +44 -0
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/instructions.js +195 -0
- package/dist/commands/knowledge.js +64 -0
- package/dist/commands/loop-benchmark.js +72 -0
- package/dist/commands/loop.js +241 -0
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/pi-prompt.js +181 -0
- package/dist/commands/pi-reuse-benchmark.js +153 -0
- package/dist/commands/plan-list.js +5 -0
- package/dist/commands/promote-run.js +29 -0
- package/dist/commands/reference-index.js +16 -0
- package/dist/commands/run-dag.js +184 -0
- package/dist/commands/spine.js +38 -0
- package/dist/commands/stats.js +84 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/study-init.js +192 -0
- package/dist/commands/workflow.js +259 -0
- package/dist/commands/worktree-create.js +31 -0
- package/dist/commands/worktree-list.js +5 -0
- package/dist/commands/worktree-remove.js +26 -0
- package/dist/cursor-worker-entry.js +8 -0
- package/dist/executors/config-core.js +55 -0
- package/dist/executors/config.js +2 -0
- package/dist/executors/cursor-artifacts.js +33 -0
- package/dist/executors/cursor-execution-log.js +81 -0
- package/dist/executors/cursor-executor-artifacts.js +135 -0
- package/dist/executors/cursor-executor.js +468 -0
- package/dist/executors/cursor-run.js +115 -0
- package/dist/executors/cursor-tool.js +94 -0
- package/dist/executors/cursor-worker-client.js +213 -0
- package/dist/executors/cursor-worker-protocol.js +18 -0
- package/dist/executors/cursor-worker-server.js +54 -0
- package/dist/executors/cursor-worker.js +3 -0
- package/dist/executors/cursor.js +6 -0
- package/dist/executors/dag-cursor-executor.js +88 -0
- package/dist/executors/dag-pi-executor.js +322 -0
- package/dist/executors/dag-static-executor.js +45 -0
- package/dist/executors/dag.js +4 -0
- package/dist/executors/index.js +8 -0
- package/dist/executors/model-routing.js +60 -0
- package/dist/executors/pi-event-serializer.js +43 -0
- package/dist/executors/pi-executor.js +606 -0
- package/dist/executors/pi-reuse-benchmark.js +316 -0
- package/dist/executors/pi-runtime-reuse.js +29 -0
- package/dist/executors/pi-sdk-executor.js +255 -0
- package/dist/executors/pi-sdk.js +1 -0
- package/dist/executors/pi.js +3 -0
- package/dist/executors/shell-executor.js +300 -0
- package/dist/executors/shell-presets.js +47 -0
- package/dist/executors/shell-verification.js +251 -0
- package/dist/executors/shell-write-guard.js +126 -0
- package/dist/executors/shell.js +3 -0
- package/dist/executors/static.js +1 -0
- package/dist/governance/checks.js +434 -0
- package/dist/governance/harness.js +9 -0
- package/dist/governance/index.js +3 -0
- package/dist/governance/manifest-types.js +128 -0
- package/dist/governance/manifest.js +2 -0
- package/dist/governance/path-guard.js +69 -0
- package/dist/governance/path-guards.js +2 -0
- package/dist/governance/profiles.js +3 -0
- package/dist/governance/requirement-coverage.js +425 -0
- package/dist/governance/skill-safety.js +135 -0
- package/dist/governance/spine-audit.js +152 -0
- package/dist/records/closeout.js +2 -0
- package/dist/records/harvest.js +236 -0
- package/dist/records/index.js +3 -0
- package/dist/records/one-shot-runs.js +421 -0
- package/dist/records/promotion.js +199 -0
- package/dist/shared/artifacts-core.js +88 -0
- package/dist/shared/artifacts.js +2 -0
- package/dist/shared/context-files.js +32 -0
- package/dist/shared/context.js +2 -0
- package/dist/shared/copy-dir.js +17 -0
- package/dist/shared/git-progress.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/logger.js +23 -0
- package/dist/shared/one-shot-prompt-args.js +98 -0
- package/dist/shared/path-refs.js +31 -0
- package/dist/shared/prompts.js +26 -0
- package/dist/shared/reference-context.js +238 -0
- package/dist/shared/timeout-policy.js +19 -0
- package/dist/shared/timeout.js +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/task/config-types.js +97 -0
- package/dist/task/config.js +2 -0
- package/dist/task/delegate.js +220 -0
- package/dist/task/goal-audit.js +51 -0
- package/dist/task/goal-policy.js +8 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/ids.js +1 -0
- package/dist/task/index.js +9 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/read-model.js +149 -0
- package/dist/task/runtime.js +699 -0
- package/dist/task/source-state.js +1 -0
- package/dist/task/state.js +55 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/task/workflow-state-types.js +92 -0
- package/dist/task/worktree-cleanup.js +140 -0
- package/dist/task/worktree.js +171 -0
- package/dist/workflows/dag/authoring.js +8 -0
- package/dist/workflows/dag/authority-surface.js +138 -0
- package/dist/workflows/dag/canvas-observer.js +474 -0
- package/dist/workflows/dag/decision-envelope.js +502 -0
- package/dist/workflows/dag/decision-evidence.js +153 -0
- package/dist/workflows/dag/decision-gates.js +1 -0
- package/dist/workflows/dag/executor-registry.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/failure-category.js +111 -0
- package/dist/workflows/dag/final-verification.js +180 -0
- package/dist/workflows/dag/governance-constants.js +5 -0
- package/dist/workflows/dag/governance-profile.js +405 -0
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +855 -0
- package/dist/workflows/dag/knowledge-curator.js +162 -0
- package/dist/workflows/dag/lifecycle.js +484 -0
- package/dist/workflows/dag/prompt-source.js +88 -0
- package/dist/workflows/dag/prompt.js +130 -0
- package/dist/workflows/dag/reconcile-tasks.js +404 -0
- package/dist/workflows/dag/recovery-recommendation.js +226 -0
- package/dist/workflows/dag/repair-artifact.js +136 -0
- package/dist/workflows/dag/report.js +1019 -0
- package/dist/workflows/dag/runner.js +1677 -0
- package/dist/workflows/dag/runtime.js +5 -0
- package/dist/workflows/dag/skill-instructions.js +471 -0
- package/dist/workflows/dag/skills.js +41 -0
- package/dist/workflows/dag/spec.js +3 -0
- package/dist/workflows/dag/topo.js +30 -0
- package/dist/workflows/dag/types.js +275 -0
- package/dist/workflows/dag/upstream-artifacts.js +95 -0
- package/dist/workflows/dag/validate.js +527 -0
- package/dist/workflows/dynamic/artifacts.js +65 -0
- package/dist/workflows/dynamic/compile.js +360 -0
- package/dist/workflows/dynamic/compileTypes.js +1 -0
- package/dist/workflows/dynamic/errors.js +5 -0
- package/dist/workflows/dynamic/index.js +7 -0
- package/dist/workflows/dynamic/profiles.js +156 -0
- package/dist/workflows/dynamic/spec.js +114 -0
- package/dist/workflows/dynamic/validate.js +275 -0
- package/dist/workflows/loop/actions.js +1334 -0
- package/dist/workflows/loop/benchmark.js +510 -0
- package/dist/workflows/loop/closeout.js +134 -0
- package/dist/workflows/loop/context.js +48 -0
- package/dist/workflows/loop/events.js +25 -0
- package/dist/workflows/loop/hash.js +32 -0
- package/dist/workflows/loop/index.js +8 -0
- package/dist/workflows/loop/paths.js +17 -0
- package/dist/workflows/loop/rounds.js +81 -0
- package/dist/workflows/loop/signals.js +55 -0
- package/dist/workflows/loop/state.js +116 -0
- package/dist/workflows/loop/templates.js +54 -0
- package/dist/workflows/loop/types.js +28 -0
- package/docs/README.md +62 -0
- package/docs/agent-dag-recovery-playbook.md +158 -0
- package/docs/agent-dag-runner.md +40 -0
- package/docs/cursor-executor-usage.md +25 -0
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +36 -0
- package/docs/development-principles.md +71 -0
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
- package/docs/exec-plans/README.md +6 -0
- package/docs/exec-plans/active/README.md +5 -0
- package/docs/exec-plans/completed/README.md +5 -0
- package/docs/feature-workflow.md +184 -0
- package/docs/harness-methodology-debugging.md +153 -0
- package/docs/harness-methodology-tdd.md +130 -0
- package/docs/harness-methodology-verification.md +27 -0
- package/docs/loop-agent-harness.md +42 -0
- package/docs/progress/README.md +3 -0
- package/docs/reports/README.md +3 -0
- package/docs/templates/adr.md +60 -0
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
- package/docs/templates/agent-dag-report.schema.json +423 -0
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
- package/docs/templates/agent-dag.base.json +195 -0
- package/docs/templates/agent-dag.final-verification.json +190 -0
- package/docs/templates/agent-dag.schema.json +316 -0
- package/docs/templates/agent-dag.supervised-implementation.json +500 -0
- package/docs/templates/exec-plan.md +64 -0
- package/docs/templates/feature-spec.md +53 -0
- package/docs/templates/hybrid-dag.json +193 -0
- package/docs/templates/progress-log.md +17 -0
- package/docs/templates/project-start-checklist.md +9 -0
- package/docs/templates/qa-report.md +42 -0
- package/docs/templates/sprint-contract.md +29 -0
- package/docs/verification-matrix.md +30 -0
- package/examples/decision-gate-agent-dag.json +123 -0
- package/examples/example-dag.json +51 -0
- package/examples/hybrid-loop-agent-dag.json +194 -0
- package/harness.json +92 -0
- package/package.json +61 -0
- package/skills/ai-engineering-context/SKILL.md +48 -0
- package/skills/loop-agent/SKILL.md +260 -0
- package/skills/loop-agent/references/README.md +63 -0
- package/skills/loop-agent/references/command-reference.md +315 -0
- package/skills/loop-agent/references/harness-policy.md +258 -0
- package/skills/loop-agent/references/hybrid-dag.md +216 -0
- package/skills/loop-agent/references/learned/README.md +21 -0
- package/skills/loop-agent/references/model-routing.md +36 -0
- package/skills/loop-agent/references/multi-worktree.md +54 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -0
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
- package/skills/loop-agent/references/pi-prompt.md +23 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
- package/skills/loop-agent/references/task-workflow.md +84 -0
- package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/verification-before-completion/SKILL.md +154 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { access, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveCliPath } from "../../shared/path-refs.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { repairArtifactSchema } from "./repair-artifact.js";
|
|
7
|
+
import { dagRunDirExists, getDagRunDir, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
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
|
+
import { dagNormalizedFailureCategorySchema, normalizeDagFailureCategory, } from "./failure-category.js";
|
|
10
|
+
import { DAG_RECOVERY_ACTIONS, planDagRecovery, } from "./recovery-recommendation.js";
|
|
11
|
+
import { dagNodeExecutorSchema, dagNodeStatusSchema, LEGACY_TOP_LEVEL_MODELS_ERROR, parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
12
|
+
export const DAG_REPORT_SCHEMA_VERSION = 1;
|
|
13
|
+
export const DAG_REPORT_SCHEMA_RELATIVE_PATH = "docs/templates/agent-dag-report.schema.json";
|
|
14
|
+
const dagRunLifecycleSchema = z.enum(["active", "paused", "completed"]);
|
|
15
|
+
const dagRunStatusSchema = z.enum([
|
|
16
|
+
"pending",
|
|
17
|
+
"running",
|
|
18
|
+
"finished",
|
|
19
|
+
"partial_failed",
|
|
20
|
+
"failed",
|
|
21
|
+
"paused",
|
|
22
|
+
]);
|
|
23
|
+
const dagRecoveryActionSchema = z.enum(DAG_RECOVERY_ACTIONS);
|
|
24
|
+
const dagArtifactRefSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
path: z.string(),
|
|
27
|
+
exists: z.boolean(),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
const dagRecoveryRecommendationSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
action: dagRecoveryActionSchema,
|
|
33
|
+
summary: z.string(),
|
|
34
|
+
reason: z.string(),
|
|
35
|
+
humanRequired: z.boolean(),
|
|
36
|
+
autoRetryEligible: z.boolean(),
|
|
37
|
+
commandHint: z.string().optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
const dagReportPrimaryFailureSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
scope: z.enum(["node", "run"]),
|
|
43
|
+
nodeId: z.string().optional(),
|
|
44
|
+
nodeStatus: dagNodeStatusSchema.optional(),
|
|
45
|
+
failureCategory: z.string().optional(),
|
|
46
|
+
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
const dagReportDownstreamSkippedNodeSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
nodeId: z.string(),
|
|
52
|
+
failureCategory: z.string().optional(),
|
|
53
|
+
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
const dagConvergencePassArtifactRefSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
nodeId: z.string(),
|
|
59
|
+
status: dagNodeStatusSchema,
|
|
60
|
+
failureCategory: z.string().optional(),
|
|
61
|
+
nodeRecordPath: z.string().optional(),
|
|
62
|
+
stdoutArtifactPath: z.string().optional(),
|
|
63
|
+
assistantArtifactPath: z.string().optional(),
|
|
64
|
+
preservedNodeRecordPath: z.string().optional(),
|
|
65
|
+
preservedNodeDir: z.string().optional(),
|
|
66
|
+
})
|
|
67
|
+
.strict();
|
|
68
|
+
const dagConvergencePassReportSchema = z
|
|
69
|
+
.object({
|
|
70
|
+
pass: z.number().int().positive(),
|
|
71
|
+
startedAt: z.string().optional(),
|
|
72
|
+
finishedAt: z.string(),
|
|
73
|
+
status: z.enum(["retrying", "terminal", "paused"]),
|
|
74
|
+
reason: z.string(),
|
|
75
|
+
hardVerifyStatus: dagNodeStatusSchema.optional(),
|
|
76
|
+
hardVerifyFailureCategory: z.string().optional(),
|
|
77
|
+
processVerdict: z.enum(["pass", "request-revision", "unknown"]).optional(),
|
|
78
|
+
repairArtifact: repairArtifactSchema.optional(),
|
|
79
|
+
verifyPhase: z.enum(["intermediate", "final"]).optional(),
|
|
80
|
+
verifyQuota: z.enum(["1", "3", "full"]).optional(),
|
|
81
|
+
verifyCommandCount: z.number().int().nonnegative().optional(),
|
|
82
|
+
verifyCommandLabels: z.array(z.string()).optional(),
|
|
83
|
+
shellSuccessCount: z.number().int().nonnegative().optional(),
|
|
84
|
+
artifactRefs: z.array(dagConvergencePassArtifactRefSchema),
|
|
85
|
+
})
|
|
86
|
+
.strict();
|
|
87
|
+
const dagConvergenceReportSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
enabled: z.boolean(),
|
|
90
|
+
maxPasses: z.number().int().positive(),
|
|
91
|
+
currentPass: z.number().int().positive(),
|
|
92
|
+
terminalReason: z.string().optional(),
|
|
93
|
+
passHistory: z.array(dagConvergencePassReportSchema),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
const dagNodeDecisionEnvelopeSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
parseOk: z.boolean(),
|
|
99
|
+
decision: z.string().optional(),
|
|
100
|
+
requiresHuman: z.boolean().optional(),
|
|
101
|
+
nextAction: z.string().optional(),
|
|
102
|
+
riskLevel: z.string().optional(),
|
|
103
|
+
source: z.enum(["assistantText", "resultSummaryFallback"]).optional(),
|
|
104
|
+
errors: z.array(z.string()).optional(),
|
|
105
|
+
})
|
|
106
|
+
.passthrough();
|
|
107
|
+
const dagNodeArtifactsReportSchema = z
|
|
108
|
+
.object({
|
|
109
|
+
resultSummary: dagArtifactRefSchema,
|
|
110
|
+
promptRedacted: dagArtifactRefSchema,
|
|
111
|
+
skills: dagArtifactRefSchema,
|
|
112
|
+
nodeRecord: dagArtifactRefSchema,
|
|
113
|
+
decisionEnvelope: dagArtifactRefSchema,
|
|
114
|
+
decisionEnvelopeValidation: dagArtifactRefSchema,
|
|
115
|
+
humanEscalationJson: dagArtifactRefSchema,
|
|
116
|
+
humanEscalationMd: dagArtifactRefSchema,
|
|
117
|
+
humanApproval: dagArtifactRefSchema,
|
|
118
|
+
humanRejection: dagArtifactRefSchema,
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
const dagNodeReportRowSchema = z
|
|
122
|
+
.object({
|
|
123
|
+
nodeId: z.string(),
|
|
124
|
+
rank: z.number().int().nonnegative(),
|
|
125
|
+
order: z.number().int().nonnegative(),
|
|
126
|
+
executor: dagNodeExecutorSchema,
|
|
127
|
+
model: z.string().optional(),
|
|
128
|
+
status: dagNodeStatusSchema,
|
|
129
|
+
durationMs: z.number().nonnegative().optional(),
|
|
130
|
+
tokensUsed: z.number().nonnegative().optional(),
|
|
131
|
+
failureCategory: z.string().optional(),
|
|
132
|
+
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
133
|
+
recoveryRecommendation: dagRecoveryRecommendationSchema.optional(),
|
|
134
|
+
backend: z.enum(["sdk", "cli"]).optional(),
|
|
135
|
+
sdkAttempted: z.boolean().optional(),
|
|
136
|
+
startedAt: z.string().optional(),
|
|
137
|
+
finishedAt: z.string().optional(),
|
|
138
|
+
decisionEnvelope: dagNodeDecisionEnvelopeSchema.optional(),
|
|
139
|
+
artifacts: dagNodeArtifactsReportSchema,
|
|
140
|
+
})
|
|
141
|
+
.strict();
|
|
142
|
+
const dagRunReportEntrySchema = z
|
|
143
|
+
.object({
|
|
144
|
+
runId: z.string().min(1),
|
|
145
|
+
title: z.string(),
|
|
146
|
+
lifecycle: dagRunLifecycleSchema,
|
|
147
|
+
status: dagRunStatusSchema,
|
|
148
|
+
runDir: z.string().min(1),
|
|
149
|
+
startedAt: z.string().min(1),
|
|
150
|
+
finishedAt: z.string().optional(),
|
|
151
|
+
failureCategory: z.string().optional(),
|
|
152
|
+
normalizedFailureCategory: dagNormalizedFailureCategorySchema.optional(),
|
|
153
|
+
recoveryRecommendation: dagRecoveryRecommendationSchema.optional(),
|
|
154
|
+
primaryFailure: dagReportPrimaryFailureSchema,
|
|
155
|
+
primaryRecovery: dagRecoveryRecommendationSchema,
|
|
156
|
+
downstreamSkippedNodes: z.array(dagReportDownstreamSkippedNodeSchema),
|
|
157
|
+
convergence: dagConvergenceReportSchema.optional(),
|
|
158
|
+
pausedByNodeId: z.string().optional(),
|
|
159
|
+
pauseReason: z.string().optional(),
|
|
160
|
+
executorJsonl: dagArtifactRefSchema,
|
|
161
|
+
nodes: z.array(dagNodeReportRowSchema),
|
|
162
|
+
})
|
|
163
|
+
.strict();
|
|
164
|
+
export const dagReportJsonSchema = z
|
|
165
|
+
.object({
|
|
166
|
+
schemaVersion: z.literal(DAG_REPORT_SCHEMA_VERSION),
|
|
167
|
+
runs: z.array(dagRunReportEntrySchema),
|
|
168
|
+
})
|
|
169
|
+
.strict();
|
|
170
|
+
export function serializeDagReportJson(report) {
|
|
171
|
+
return {
|
|
172
|
+
schemaVersion: DAG_REPORT_SCHEMA_VERSION,
|
|
173
|
+
runs: report.runs,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function validateDagReportJson(value) {
|
|
177
|
+
const parsed = dagReportJsonSchema.safeParse(value);
|
|
178
|
+
if (parsed.success) {
|
|
179
|
+
return { ok: true, report: parsed.data };
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
errors: parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export const DAG_REPORT_LIFECYCLE_ORDER = [
|
|
187
|
+
"active",
|
|
188
|
+
"paused",
|
|
189
|
+
"completed",
|
|
190
|
+
];
|
|
191
|
+
export const DAG_PAUSED_LATEST_REPORT_PRESET = {
|
|
192
|
+
lifecycle: "paused",
|
|
193
|
+
latest: true,
|
|
194
|
+
};
|
|
195
|
+
export { DAG_RECOVERY_ACTIONS };
|
|
196
|
+
export async function probeArtifact(filePath) {
|
|
197
|
+
try {
|
|
198
|
+
await access(filePath);
|
|
199
|
+
return { path: filePath, exists: true };
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return { path: filePath, exists: false };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export async function buildNodeArtifactsReport(runDir, nodeId) {
|
|
206
|
+
const nodeDir = path.join(runDir, nodeId);
|
|
207
|
+
const artifact = (name) => probeArtifact(path.join(nodeDir, name));
|
|
208
|
+
return {
|
|
209
|
+
resultSummary: await artifact("result.summary.md"),
|
|
210
|
+
promptRedacted: await artifact("prompt.redacted.md"),
|
|
211
|
+
skills: await artifact("skills.json"),
|
|
212
|
+
nodeRecord: await probeArtifact(path.join(runDir, `${nodeId}.json`)),
|
|
213
|
+
decisionEnvelope: await artifact("decision.envelope.json"),
|
|
214
|
+
decisionEnvelopeValidation: await artifact("decision.envelope.validation.json"),
|
|
215
|
+
humanEscalationJson: await artifact("human-escalation.json"),
|
|
216
|
+
humanEscalationMd: await artifact("human-escalation.md"),
|
|
217
|
+
humanApproval: await artifact("human-approval.json"),
|
|
218
|
+
humanRejection: await artifact("human-rejection.json"),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function taskById(spec) {
|
|
222
|
+
return new Map(spec.tasks.map((task) => [task.id, task]));
|
|
223
|
+
}
|
|
224
|
+
function isFailedRunStatus(status) {
|
|
225
|
+
return status === "failed" || status === "partial_failed";
|
|
226
|
+
}
|
|
227
|
+
function isActionableNodeForRecovery(node) {
|
|
228
|
+
return (node.status === "ERROR" ||
|
|
229
|
+
node.status === "SKIPPED" ||
|
|
230
|
+
(node.failureCategory !== undefined && node.failureCategory !== "success"));
|
|
231
|
+
}
|
|
232
|
+
function findPrimaryActionableNodeForRecovery(nodes) {
|
|
233
|
+
return (nodes.find((node) => node.status === "ERROR") ??
|
|
234
|
+
nodes.find(isActionableNodeForRecovery));
|
|
235
|
+
}
|
|
236
|
+
function resolvePrimaryFailureNode(run) {
|
|
237
|
+
if (run.pausedByNodeId) {
|
|
238
|
+
return run.nodes.find((node) => node.nodeId === run.pausedByNodeId);
|
|
239
|
+
}
|
|
240
|
+
if (isFailedRunStatus(run.status)) {
|
|
241
|
+
return findPrimaryActionableNodeForRecovery(run.nodes);
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
function buildPrimaryFailure(run, primaryNode) {
|
|
246
|
+
if (primaryNode) {
|
|
247
|
+
return {
|
|
248
|
+
scope: "node",
|
|
249
|
+
nodeId: primaryNode.nodeId,
|
|
250
|
+
nodeStatus: primaryNode.status,
|
|
251
|
+
failureCategory: primaryNode.failureCategory,
|
|
252
|
+
normalizedFailureCategory: primaryNode.normalizedFailureCategory,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
scope: "run",
|
|
257
|
+
failureCategory: run.failureCategory,
|
|
258
|
+
normalizedFailureCategory: run.normalizedFailureCategory,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function resolvePrimaryRecovery(run, primaryNode) {
|
|
262
|
+
if (run.recoveryRecommendation) {
|
|
263
|
+
return run.recoveryRecommendation;
|
|
264
|
+
}
|
|
265
|
+
if (primaryNode?.recoveryRecommendation) {
|
|
266
|
+
return primaryNode.recoveryRecommendation;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
action: "none",
|
|
270
|
+
summary: "No recovery needed.",
|
|
271
|
+
reason: `${run.runId} finished without a derived recovery recommendation.`,
|
|
272
|
+
humanRequired: false,
|
|
273
|
+
autoRetryEligible: false,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function collectTransitiveDescendantNodeIds(spec, rootNodeId) {
|
|
277
|
+
const childrenByParent = new Map();
|
|
278
|
+
for (const task of spec.tasks) {
|
|
279
|
+
for (const parent of task.depends_on) {
|
|
280
|
+
const children = childrenByParent.get(parent) ?? [];
|
|
281
|
+
children.push(task.id);
|
|
282
|
+
childrenByParent.set(parent, children);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const descendants = new Set();
|
|
286
|
+
const queue = [...(childrenByParent.get(rootNodeId) ?? [])];
|
|
287
|
+
while (queue.length > 0) {
|
|
288
|
+
const nodeId = queue.shift();
|
|
289
|
+
if (descendants.has(nodeId))
|
|
290
|
+
continue;
|
|
291
|
+
descendants.add(nodeId);
|
|
292
|
+
queue.push(...(childrenByParent.get(nodeId) ?? []));
|
|
293
|
+
}
|
|
294
|
+
return descendants;
|
|
295
|
+
}
|
|
296
|
+
function collectDownstreamSkippedNodes(nodes, spec, primaryNode) {
|
|
297
|
+
const skippedNodes = nodes.filter((node) => node.status === "SKIPPED");
|
|
298
|
+
const downstream = primaryNode
|
|
299
|
+
? skippedNodes.filter((node) => collectTransitiveDescendantNodeIds(spec, primaryNode.nodeId).has(node.nodeId))
|
|
300
|
+
: skippedNodes;
|
|
301
|
+
return downstream.map((node) => ({
|
|
302
|
+
nodeId: node.nodeId,
|
|
303
|
+
failureCategory: node.failureCategory,
|
|
304
|
+
normalizedFailureCategory: node.normalizedFailureCategory,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
function isFailedReportRun(run) {
|
|
308
|
+
return (run.status === "failed" ||
|
|
309
|
+
run.status === "partial_failed" ||
|
|
310
|
+
run.status === "paused" ||
|
|
311
|
+
(run.primaryRecovery.action !== "none" &&
|
|
312
|
+
run.primaryRecovery.action !== "monitor"));
|
|
313
|
+
}
|
|
314
|
+
export function filterDagReportRuns(runs, options) {
|
|
315
|
+
let filtered = runs;
|
|
316
|
+
if (options.failedOnly) {
|
|
317
|
+
filtered = filtered.filter(isFailedReportRun);
|
|
318
|
+
}
|
|
319
|
+
if (options.action) {
|
|
320
|
+
filtered = filtered.filter((run) => run.primaryRecovery.action === options.action);
|
|
321
|
+
}
|
|
322
|
+
if (options.latest && filtered.length > 0) {
|
|
323
|
+
const latest = [...filtered].sort((left, right) => {
|
|
324
|
+
const startedCompare = left.startedAt.localeCompare(right.startedAt);
|
|
325
|
+
if (startedCompare !== 0)
|
|
326
|
+
return startedCompare;
|
|
327
|
+
return left.runId.localeCompare(right.runId);
|
|
328
|
+
});
|
|
329
|
+
return [latest[latest.length - 1]];
|
|
330
|
+
}
|
|
331
|
+
return filtered;
|
|
332
|
+
}
|
|
333
|
+
export async function buildDagRunReportEntry(input) {
|
|
334
|
+
const tasks = taskById(input.spec);
|
|
335
|
+
const nodes = [];
|
|
336
|
+
for (let rank = 0; rank < input.state.ranks.length; rank += 1) {
|
|
337
|
+
const rankNodes = input.state.ranks[rank] ?? [];
|
|
338
|
+
for (let order = 0; order < rankNodes.length; order += 1) {
|
|
339
|
+
const nodeId = rankNodes[order];
|
|
340
|
+
const node = input.state.nodes[nodeId];
|
|
341
|
+
if (!node)
|
|
342
|
+
continue;
|
|
343
|
+
const task = tasks.get(nodeId);
|
|
344
|
+
const normalizedFailureCategory = normalizeDagFailureCategory(node.failureCategory, node.status);
|
|
345
|
+
nodes.push({
|
|
346
|
+
nodeId,
|
|
347
|
+
rank,
|
|
348
|
+
order,
|
|
349
|
+
executor: node.executor,
|
|
350
|
+
model: task
|
|
351
|
+
? resolveModelForTask(task, input.spec.executorModels)
|
|
352
|
+
: undefined,
|
|
353
|
+
status: node.status,
|
|
354
|
+
durationMs: node.durationMs,
|
|
355
|
+
tokensUsed: node.tokensUsed,
|
|
356
|
+
failureCategory: node.failureCategory,
|
|
357
|
+
normalizedFailureCategory,
|
|
358
|
+
recoveryRecommendation: planDagRecovery({
|
|
359
|
+
status: node.status,
|
|
360
|
+
normalizedFailureCategory,
|
|
361
|
+
failureCategory: node.failureCategory,
|
|
362
|
+
nodeId,
|
|
363
|
+
runId: input.state.runId,
|
|
364
|
+
lifecycle: input.lifecycle,
|
|
365
|
+
runStatus: input.state.status,
|
|
366
|
+
}),
|
|
367
|
+
backend: node.backend,
|
|
368
|
+
sdkAttempted: node.sdkAttempted,
|
|
369
|
+
startedAt: node.startedAt,
|
|
370
|
+
finishedAt: node.finishedAt,
|
|
371
|
+
decisionEnvelope: node.decisionEnvelope,
|
|
372
|
+
artifacts: await buildNodeArtifactsReport(input.runDir, nodeId),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const normalizedFailureCategory = normalizeDagFailureCategory(input.state.failureCategory, input.state.status);
|
|
377
|
+
const pausedNode = input.state.pausedByNodeId
|
|
378
|
+
? input.state.nodes[input.state.pausedByNodeId]
|
|
379
|
+
: undefined;
|
|
380
|
+
const firstActionableNode = isFailedRunStatus(input.state.status)
|
|
381
|
+
? findPrimaryActionableNodeForRecovery(nodes)
|
|
382
|
+
: undefined;
|
|
383
|
+
const runRecoverySource = pausedNode
|
|
384
|
+
? {
|
|
385
|
+
status: input.state.status,
|
|
386
|
+
normalizedFailureCategory: normalizeDagFailureCategory(pausedNode.failureCategory, pausedNode.status),
|
|
387
|
+
failureCategory: pausedNode.failureCategory,
|
|
388
|
+
nodeId: input.state.pausedByNodeId,
|
|
389
|
+
}
|
|
390
|
+
: firstActionableNode
|
|
391
|
+
? {
|
|
392
|
+
status: firstActionableNode.status,
|
|
393
|
+
normalizedFailureCategory: firstActionableNode.normalizedFailureCategory ??
|
|
394
|
+
normalizeDagFailureCategory(firstActionableNode.failureCategory, firstActionableNode.status),
|
|
395
|
+
failureCategory: firstActionableNode.failureCategory,
|
|
396
|
+
nodeId: firstActionableNode.nodeId,
|
|
397
|
+
}
|
|
398
|
+
: {
|
|
399
|
+
status: input.state.status,
|
|
400
|
+
normalizedFailureCategory,
|
|
401
|
+
failureCategory: input.state.failureCategory,
|
|
402
|
+
};
|
|
403
|
+
const recoveryRecommendation = planDagRecovery({
|
|
404
|
+
...runRecoverySource,
|
|
405
|
+
runId: input.state.runId,
|
|
406
|
+
lifecycle: input.lifecycle,
|
|
407
|
+
runStatus: input.state.status,
|
|
408
|
+
});
|
|
409
|
+
const partialEntry = {
|
|
410
|
+
runId: input.state.runId,
|
|
411
|
+
title: input.state.title,
|
|
412
|
+
lifecycle: input.lifecycle,
|
|
413
|
+
status: input.state.status,
|
|
414
|
+
runDir: input.runDir,
|
|
415
|
+
startedAt: input.state.startedAt,
|
|
416
|
+
finishedAt: input.state.finishedAt,
|
|
417
|
+
failureCategory: input.state.failureCategory,
|
|
418
|
+
normalizedFailureCategory,
|
|
419
|
+
recoveryRecommendation,
|
|
420
|
+
pausedByNodeId: input.state.pausedByNodeId,
|
|
421
|
+
pauseReason: input.state.pauseReason,
|
|
422
|
+
nodes,
|
|
423
|
+
};
|
|
424
|
+
const primaryNode = resolvePrimaryFailureNode(partialEntry);
|
|
425
|
+
return {
|
|
426
|
+
...partialEntry,
|
|
427
|
+
primaryFailure: buildPrimaryFailure(partialEntry, primaryNode),
|
|
428
|
+
primaryRecovery: resolvePrimaryRecovery(partialEntry, primaryNode),
|
|
429
|
+
downstreamSkippedNodes: collectDownstreamSkippedNodes(nodes, input.spec, primaryNode),
|
|
430
|
+
convergence: input.state.convergence,
|
|
431
|
+
executorJsonl: await probeArtifact(path.join(input.runDir, "executor.jsonl")),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async function readDagRunSpecForReport(runDir) {
|
|
435
|
+
try {
|
|
436
|
+
return await readDagRunSpec(runDir);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
+
if (!message.includes(LEGACY_TOP_LEVEL_MODELS_ERROR)) {
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
const raw = JSON.parse(await readFile(path.join(runDir, "run.json"), "utf-8"));
|
|
444
|
+
delete raw.models;
|
|
445
|
+
return parseDagSpec(raw);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function readLocatedRun(located) {
|
|
449
|
+
const [state, spec] = await Promise.all([
|
|
450
|
+
readDagRunState(located.runDir),
|
|
451
|
+
readDagRunSpecForReport(located.runDir),
|
|
452
|
+
]);
|
|
453
|
+
return buildDagRunReportEntry({
|
|
454
|
+
runDir: located.runDir,
|
|
455
|
+
lifecycle: located.lifecycle,
|
|
456
|
+
state,
|
|
457
|
+
spec,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async function listRunsInLifecycle(repoRoot, lifecycle) {
|
|
461
|
+
const lifecycleDir = getDagRunDir(repoRoot, lifecycle, "");
|
|
462
|
+
try {
|
|
463
|
+
await access(lifecycleDir);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
const entries = await readdir(lifecycleDir, { withFileTypes: true });
|
|
469
|
+
const located = [];
|
|
470
|
+
for (const entry of entries) {
|
|
471
|
+
if (!entry.isDirectory())
|
|
472
|
+
continue;
|
|
473
|
+
const runDir = path.join(lifecycleDir, entry.name);
|
|
474
|
+
const statePath = path.join(runDir, "state.json");
|
|
475
|
+
try {
|
|
476
|
+
await access(statePath);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
located.push({
|
|
482
|
+
lifecycle,
|
|
483
|
+
runDir,
|
|
484
|
+
runId: entry.name,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return located;
|
|
488
|
+
}
|
|
489
|
+
async function sortLocatedRuns(located) {
|
|
490
|
+
const withStartedAt = await Promise.all(located.map(async (entry) => {
|
|
491
|
+
try {
|
|
492
|
+
const state = await readDagRunState(entry.runDir);
|
|
493
|
+
return { entry, startedAt: state.startedAt };
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return { entry, startedAt: "" };
|
|
497
|
+
}
|
|
498
|
+
}));
|
|
499
|
+
return withStartedAt
|
|
500
|
+
.sort((left, right) => {
|
|
501
|
+
const startedCompare = left.startedAt.localeCompare(right.startedAt);
|
|
502
|
+
if (startedCompare !== 0)
|
|
503
|
+
return startedCompare;
|
|
504
|
+
return left.entry.runId.localeCompare(right.entry.runId);
|
|
505
|
+
})
|
|
506
|
+
.map((item) => item.entry);
|
|
507
|
+
}
|
|
508
|
+
export async function listLocatedDagRuns(repoRoot, filter = "all") {
|
|
509
|
+
const lifecycles = filter === "all" ? DAG_REPORT_LIFECYCLE_ORDER : [filter];
|
|
510
|
+
const located = [];
|
|
511
|
+
for (const lifecycle of lifecycles) {
|
|
512
|
+
const runs = await listRunsInLifecycle(repoRoot, lifecycle);
|
|
513
|
+
located.push(...(await sortLocatedRuns(runs)));
|
|
514
|
+
}
|
|
515
|
+
return located;
|
|
516
|
+
}
|
|
517
|
+
async function locateRunForReport(repoRoot, runId, filter) {
|
|
518
|
+
if (filter === "all") {
|
|
519
|
+
const located = await locateDagRun(repoRoot, runId);
|
|
520
|
+
if (!located) {
|
|
521
|
+
throw new Error(`dag run not found: ${runId}`);
|
|
522
|
+
}
|
|
523
|
+
return { ...located, runId };
|
|
524
|
+
}
|
|
525
|
+
const runDir = getDagRunDir(repoRoot, filter, runId);
|
|
526
|
+
if (!(await dagRunDirExists(runDir))) {
|
|
527
|
+
throw new Error(`dag run not found: ${runId}`);
|
|
528
|
+
}
|
|
529
|
+
return { lifecycle: filter, runDir, runId };
|
|
530
|
+
}
|
|
531
|
+
export async function buildDagReport(options) {
|
|
532
|
+
const filter = options.lifecycle ?? "all";
|
|
533
|
+
const located = options.runId
|
|
534
|
+
? [await locateRunForReport(options.repoRoot, options.runId, filter)]
|
|
535
|
+
: await listLocatedDagRuns(options.repoRoot, filter);
|
|
536
|
+
const runs = await Promise.all(located.map((entry) => readLocatedRun(entry)));
|
|
537
|
+
return {
|
|
538
|
+
runs: filterDagReportRuns(runs, {
|
|
539
|
+
failedOnly: options.failedOnly,
|
|
540
|
+
latest: options.latest,
|
|
541
|
+
action: options.action,
|
|
542
|
+
}),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function formatDuration(durationMs) {
|
|
546
|
+
if (durationMs === undefined)
|
|
547
|
+
return "-";
|
|
548
|
+
if (durationMs < 1000)
|
|
549
|
+
return `${durationMs}ms`;
|
|
550
|
+
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
551
|
+
}
|
|
552
|
+
function formatTokens(tokensUsed) {
|
|
553
|
+
return tokensUsed === undefined ? "-" : String(tokensUsed);
|
|
554
|
+
}
|
|
555
|
+
function formatArtifactFlags(artifacts) {
|
|
556
|
+
const flags = [];
|
|
557
|
+
if (artifacts.resultSummary.exists)
|
|
558
|
+
flags.push("summary");
|
|
559
|
+
if (artifacts.promptRedacted.exists)
|
|
560
|
+
flags.push("prompt");
|
|
561
|
+
if (artifacts.skills.exists)
|
|
562
|
+
flags.push("skills");
|
|
563
|
+
if (artifacts.nodeRecord.exists)
|
|
564
|
+
flags.push("node-json");
|
|
565
|
+
if (artifacts.decisionEnvelope.exists)
|
|
566
|
+
flags.push("decision");
|
|
567
|
+
if (artifacts.decisionEnvelopeValidation.exists)
|
|
568
|
+
flags.push("decision-validation");
|
|
569
|
+
if (artifacts.humanEscalationJson.exists)
|
|
570
|
+
flags.push("escalation-json");
|
|
571
|
+
if (artifacts.humanEscalationMd.exists)
|
|
572
|
+
flags.push("escalation-md");
|
|
573
|
+
if (artifacts.humanApproval.exists)
|
|
574
|
+
flags.push("approval");
|
|
575
|
+
if (artifacts.humanRejection.exists)
|
|
576
|
+
flags.push("rejection");
|
|
577
|
+
return flags.length > 0 ? flags.join(",") : "-";
|
|
578
|
+
}
|
|
579
|
+
function formatTimestamp(value) {
|
|
580
|
+
return value ?? "—";
|
|
581
|
+
}
|
|
582
|
+
function formatRunDuration(startedAt, finishedAt) {
|
|
583
|
+
if (!finishedAt)
|
|
584
|
+
return "—";
|
|
585
|
+
const started = Date.parse(startedAt);
|
|
586
|
+
const finished = Date.parse(finishedAt);
|
|
587
|
+
if (!Number.isFinite(started) || !Number.isFinite(finished))
|
|
588
|
+
return "—";
|
|
589
|
+
return formatDuration(Math.max(0, finished - started));
|
|
590
|
+
}
|
|
591
|
+
function summarizeNodeStatuses(nodes) {
|
|
592
|
+
const total = nodes.length;
|
|
593
|
+
const finished = nodes.filter((node) => node.status === "FINISHED").length;
|
|
594
|
+
const failed = nodes.filter((node) => node.status === "ERROR").length;
|
|
595
|
+
const skipped = nodes.filter((node) => node.status === "SKIPPED").length;
|
|
596
|
+
const pending = nodes.filter((node) => node.status === "PENDING").length;
|
|
597
|
+
return `${total} total / ${finished} finished / ${failed} failed / ${skipped} skipped / ${pending} pending`;
|
|
598
|
+
}
|
|
599
|
+
function hasArtifact(artifacts) {
|
|
600
|
+
return Object.values(artifacts).some((artifact) => artifact.exists);
|
|
601
|
+
}
|
|
602
|
+
function failureRows(run) {
|
|
603
|
+
return run.nodes.filter((node) => node.status === "ERROR" ||
|
|
604
|
+
(node.failureCategory !== undefined &&
|
|
605
|
+
node.failureCategory !== "success"));
|
|
606
|
+
}
|
|
607
|
+
function formatFailureCategoryLabel(node) {
|
|
608
|
+
const raw = node.failureCategory ?? node.status;
|
|
609
|
+
const normalized = node.normalizedFailureCategory;
|
|
610
|
+
if (normalized && normalized !== raw) {
|
|
611
|
+
return `${raw} (normalized: ${normalized})`;
|
|
612
|
+
}
|
|
613
|
+
return String(raw);
|
|
614
|
+
}
|
|
615
|
+
function primaryRecoveryRecommendation(run) {
|
|
616
|
+
return run.primaryRecovery;
|
|
617
|
+
}
|
|
618
|
+
function suggestedNextAction(run) {
|
|
619
|
+
const humanNodes = run.nodes.filter((node) => node.decisionEnvelope?.requiresHuman === true);
|
|
620
|
+
const humanSuffix = humanNodes.length > 0
|
|
621
|
+
? ` Human escalation detected for ${humanNodes.map((node) => node.nodeId).join(", ")}.`
|
|
622
|
+
: "";
|
|
623
|
+
const recommendation = primaryRecoveryRecommendation(run);
|
|
624
|
+
if (recommendation.action !== "none") {
|
|
625
|
+
const pausedHint = run.status === "paused" && run.pausedByNodeId
|
|
626
|
+
? ` Paused at node ${run.pausedByNodeId}.`
|
|
627
|
+
: "";
|
|
628
|
+
const failedHint = run.primaryFailure.scope === "node" && run.primaryFailure.nodeId
|
|
629
|
+
? ` Primary node: ${run.primaryFailure.nodeId}.`
|
|
630
|
+
: "";
|
|
631
|
+
return `${recommendation.summary} [${recommendation.action}]${pausedHint}${failedHint}${humanSuffix}`;
|
|
632
|
+
}
|
|
633
|
+
return `Run complete. Review result summaries.${humanSuffix}`;
|
|
634
|
+
}
|
|
635
|
+
function formatPrimaryFailureSection(run) {
|
|
636
|
+
const failure = run.primaryFailure;
|
|
637
|
+
if (failure.scope === "node" && failure.nodeId) {
|
|
638
|
+
return [
|
|
639
|
+
`- **Node**: ${failure.nodeId} (${failure.nodeStatus ?? "unknown"})`,
|
|
640
|
+
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
641
|
+
];
|
|
642
|
+
}
|
|
643
|
+
return [
|
|
644
|
+
`- **Scope**: run`,
|
|
645
|
+
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
function formatPrimaryRecoverySection(run) {
|
|
649
|
+
const rec = run.primaryRecovery;
|
|
650
|
+
return [
|
|
651
|
+
`- **Action**: \`${rec.action}\``,
|
|
652
|
+
`- **Summary**: ${rec.summary}`,
|
|
653
|
+
`- **Reason**: ${rec.reason}`,
|
|
654
|
+
`- **Human Required**: ${rec.humanRequired}`,
|
|
655
|
+
`- **Auto Retry Eligible**: ${rec.autoRetryEligible}`,
|
|
656
|
+
rec.commandHint ? `- **Command Hint**: ${rec.commandHint}` : undefined,
|
|
657
|
+
].filter((line) => line !== undefined);
|
|
658
|
+
}
|
|
659
|
+
function formatDownstreamSkippedSection(run) {
|
|
660
|
+
if (run.downstreamSkippedNodes.length === 0) {
|
|
661
|
+
return ["No blocked downstream or skipped nodes."];
|
|
662
|
+
}
|
|
663
|
+
return run.downstreamSkippedNodes.map((node) => `- **${node.nodeId}**: ${node.failureCategory ?? "-"} (normalized: ${node.normalizedFailureCategory ?? "-"})`);
|
|
664
|
+
}
|
|
665
|
+
function formatConvergenceSummary(run) {
|
|
666
|
+
if (!run.convergence)
|
|
667
|
+
return [];
|
|
668
|
+
const convergence = run.convergence;
|
|
669
|
+
const lines = [
|
|
670
|
+
`- convergence: enabled=${convergence.enabled}, pass=${convergence.currentPass}/${convergence.maxPasses}, terminal=${convergence.terminalReason ?? "-"}`,
|
|
671
|
+
];
|
|
672
|
+
for (const pass of convergence.passHistory) {
|
|
673
|
+
const verifySummary = pass.verifyPhase || pass.verifyQuota || pass.verifyCommandCount !== undefined
|
|
674
|
+
? `, verify=${pass.verifyPhase ?? "-"} quota=${pass.verifyQuota ?? "-"} commands=${pass.verifyCommandCount ?? "-"}`
|
|
675
|
+
: "";
|
|
676
|
+
lines.push(`- pass ${pass.pass}: ${pass.status}, reason=${pass.reason}, hardVerify=${pass.hardVerifyStatus ?? "-"}(${pass.hardVerifyFailureCategory ?? "-"})${verifySummary}, artifacts=${pass.artifactRefs.length}`);
|
|
677
|
+
}
|
|
678
|
+
return lines;
|
|
679
|
+
}
|
|
680
|
+
function formatRecommendedOperatorAction(run) {
|
|
681
|
+
const rec = run.primaryRecovery;
|
|
682
|
+
if (rec.action === "none" || rec.action === "monitor") {
|
|
683
|
+
return [
|
|
684
|
+
"No operator recovery action required; review run artifacts as needed.",
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
const lines = [`${rec.summary} [${rec.action}]`];
|
|
688
|
+
if (rec.commandHint) {
|
|
689
|
+
lines.push(`Hint: ${rec.commandHint}`);
|
|
690
|
+
}
|
|
691
|
+
if (rec.humanRequired) {
|
|
692
|
+
lines.push("Human input is required before proceeding.");
|
|
693
|
+
}
|
|
694
|
+
return lines.map((line) => `- ${line}`);
|
|
695
|
+
}
|
|
696
|
+
function formatRecoveryPlanSection(run) {
|
|
697
|
+
return [
|
|
698
|
+
"### Primary Failure",
|
|
699
|
+
...formatPrimaryFailureSection(run),
|
|
700
|
+
"",
|
|
701
|
+
"### Recovery Action",
|
|
702
|
+
...formatPrimaryRecoverySection(run),
|
|
703
|
+
"",
|
|
704
|
+
"### Blocked Downstream / Skipped Nodes",
|
|
705
|
+
...formatDownstreamSkippedSection(run),
|
|
706
|
+
"",
|
|
707
|
+
"### Recommended Operator Action",
|
|
708
|
+
...formatRecommendedOperatorAction(run),
|
|
709
|
+
];
|
|
710
|
+
}
|
|
711
|
+
export function formatDagReportMarkdown(report) {
|
|
712
|
+
if (report.runs.length === 0) {
|
|
713
|
+
return "No DAG runs matched the report filter.\n";
|
|
714
|
+
}
|
|
715
|
+
const sections = report.runs.map((run) => {
|
|
716
|
+
const header = [
|
|
717
|
+
`## ${run.runId}`,
|
|
718
|
+
"",
|
|
719
|
+
`- lifecycle: ${run.lifecycle}`,
|
|
720
|
+
`- status: ${run.status}`,
|
|
721
|
+
`- title: ${run.title}`,
|
|
722
|
+
`- runDir: ${run.runDir}`,
|
|
723
|
+
`- startedAt: ${run.startedAt}`,
|
|
724
|
+
`- finishedAt: ${run.finishedAt ?? "-"}`,
|
|
725
|
+
`- executorJsonl: ${run.executorJsonl.path} (${run.executorJsonl.exists ? "exists" : "missing"})`,
|
|
726
|
+
...formatConvergenceSummary(run),
|
|
727
|
+
"",
|
|
728
|
+
"| rank | order | node | executor | model | status | duration | tokens | failure | normalized | action | backend | sdk | artifacts |",
|
|
729
|
+
"| ---: | ----: | --- | --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
|
|
730
|
+
];
|
|
731
|
+
for (const node of run.nodes) {
|
|
732
|
+
header.push(`| ${node.rank} | ${node.order} | ${node.nodeId} | ${node.executor} | ${node.model ?? "-"} | ${node.status} | ${formatDuration(node.durationMs)} | ${formatTokens(node.tokensUsed)} | ${node.failureCategory ?? "-"} | ${node.normalizedFailureCategory ?? "-"} | ${node.recoveryRecommendation?.action ?? "-"} | ${node.backend ?? "-"} | ${node.sdkAttempted === undefined ? "-" : node.sdkAttempted ? "yes" : "no"} | ${formatArtifactFlags(node.artifacts)} |`);
|
|
733
|
+
}
|
|
734
|
+
header.push("");
|
|
735
|
+
return header.join("\n");
|
|
736
|
+
});
|
|
737
|
+
return ["# DAG Run Report", "", ...sections].join("\n");
|
|
738
|
+
}
|
|
739
|
+
function formatFailureSection(run) {
|
|
740
|
+
const failures = failureRows(run);
|
|
741
|
+
if (failures.length === 0)
|
|
742
|
+
return ["No failed nodes."];
|
|
743
|
+
return failures.map((node) => {
|
|
744
|
+
const decision = node.decisionEnvelope;
|
|
745
|
+
const decisionText = decision
|
|
746
|
+
? ` decision=${decision.decision ?? "-"}, risk=${decision.riskLevel ?? "-"}, requiresHuman=${decision.requiresHuman === undefined ? "-" : String(decision.requiresHuman)}`
|
|
747
|
+
: "";
|
|
748
|
+
return `- **${node.nodeId}**: ${formatFailureCategoryLabel(node)} (${formatDuration(node.durationMs)})${decisionText}`;
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
function formatArtifactsSection(run) {
|
|
752
|
+
const rows = run.nodes
|
|
753
|
+
.filter((node) => hasArtifact(node.artifacts))
|
|
754
|
+
.map((node) => `- **${node.nodeId}**: ${formatArtifactFlags(node.artifacts)}`);
|
|
755
|
+
return rows.length > 0 ? rows : ["No node artifacts found."];
|
|
756
|
+
}
|
|
757
|
+
export function formatDagReportHandoffMarkdown(report) {
|
|
758
|
+
if (report.runs.length === 0) {
|
|
759
|
+
return "# Agent DAG Handoff Report\n\nNo DAG runs matched the report filter.\n";
|
|
760
|
+
}
|
|
761
|
+
const sections = report.runs.map((run) => {
|
|
762
|
+
const lines = [
|
|
763
|
+
`# Agent DAG Handoff Report: ${run.runId}`,
|
|
764
|
+
"",
|
|
765
|
+
"## Summary",
|
|
766
|
+
`- **Title**: ${run.title}`,
|
|
767
|
+
`- **Run ID**: ${run.runId}`,
|
|
768
|
+
`- **Lifecycle**: ${run.lifecycle}`,
|
|
769
|
+
`- **Status**: ${run.status}`,
|
|
770
|
+
run.failureCategory
|
|
771
|
+
? `- **Failure (raw/normalized)**: ${run.failureCategory} / ${run.normalizedFailureCategory ?? "-"}`
|
|
772
|
+
: undefined,
|
|
773
|
+
`- **Started**: ${formatTimestamp(run.startedAt)}`,
|
|
774
|
+
`- **Finished**: ${formatTimestamp(run.finishedAt)}`,
|
|
775
|
+
`- **Duration**: ${formatRunDuration(run.startedAt, run.finishedAt)}`,
|
|
776
|
+
`- **Nodes**: ${summarizeNodeStatuses(run.nodes)}`,
|
|
777
|
+
`- **Run Dir**: ${run.runDir}`,
|
|
778
|
+
...formatConvergenceSummary(run),
|
|
779
|
+
"",
|
|
780
|
+
"## Node Timeline",
|
|
781
|
+
];
|
|
782
|
+
for (const node of run.nodes) {
|
|
783
|
+
lines.push(`### ${node.nodeId}`, `- **Rank/Order**: ${node.rank}/${node.order}`, `- **Status**: ${node.status}`, node.failureCategory
|
|
784
|
+
? `- **Failure (raw/normalized)**: ${node.failureCategory} / ${node.normalizedFailureCategory ?? "-"}`
|
|
785
|
+
: "", `- **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
|
+
}
|
|
787
|
+
lines.push("## Failures", ...formatFailureSection(run), "", "## Recovery Plan", ...formatRecoveryPlanSection(run), "", "## Artifacts", ...formatArtifactsSection(run), "", "## Suggested Next Action", `- ${suggestedNextAction(run)}`, "");
|
|
788
|
+
return lines
|
|
789
|
+
.filter((line) => line !== undefined)
|
|
790
|
+
.join("\n");
|
|
791
|
+
});
|
|
792
|
+
return sections.join("\n---\n");
|
|
793
|
+
}
|
|
794
|
+
export function resolveCloseoutDraftOutputPath(repoRoot, outputPath) {
|
|
795
|
+
return resolveCliPath(outputPath, repoRoot);
|
|
796
|
+
}
|
|
797
|
+
export function defaultCloseoutDraftOutputPath(runId) {
|
|
798
|
+
return path.join(os.tmpdir(), `${runId}-closeout-draft.md`);
|
|
799
|
+
}
|
|
800
|
+
export function assertCloseoutDraftOutputPathAllowed(repoRoot, outputPath) {
|
|
801
|
+
const resolved = resolveCloseoutDraftOutputPath(repoRoot, outputPath);
|
|
802
|
+
const harnessRoot = path.resolve(repoRoot, ".harness");
|
|
803
|
+
const dagRunsRoot = path.resolve(repoRoot, ".harness", "dag-runs");
|
|
804
|
+
const underHarness = resolved === harnessRoot ||
|
|
805
|
+
resolved.startsWith(`${harnessRoot}${path.sep}`);
|
|
806
|
+
if (!underHarness) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const underDagRuns = resolved === dagRunsRoot ||
|
|
810
|
+
resolved.startsWith(`${dagRunsRoot}${path.sep}`);
|
|
811
|
+
if (underDagRuns) {
|
|
812
|
+
throw new Error(`closeout draft output must not target .harness/dag-runs: ${outputPath}`);
|
|
813
|
+
}
|
|
814
|
+
throw new Error(`closeout draft output must not target .harness: ${outputPath}`);
|
|
815
|
+
}
|
|
816
|
+
function formatCloseoutNodeStatusTable(run) {
|
|
817
|
+
const lines = [
|
|
818
|
+
"| rank | node | executor | status | failure | normalized | duration |",
|
|
819
|
+
"| ---: | --- | --- | --- | --- | --- | --- |",
|
|
820
|
+
];
|
|
821
|
+
for (const node of run.nodes) {
|
|
822
|
+
lines.push(`| ${node.rank} | ${node.nodeId} | ${node.executor} | ${node.status} | ${node.failureCategory ?? "-"} | ${node.normalizedFailureCategory ?? "-"} | ${formatDuration(node.durationMs)} |`);
|
|
823
|
+
}
|
|
824
|
+
return lines;
|
|
825
|
+
}
|
|
826
|
+
function formatCloseoutArtifactRefs(artifacts) {
|
|
827
|
+
const refs = [];
|
|
828
|
+
if (artifacts.resultSummary.exists)
|
|
829
|
+
refs.push(`summary:${artifacts.resultSummary.path}`);
|
|
830
|
+
if (artifacts.nodeRecord.exists)
|
|
831
|
+
refs.push(`node-json:${artifacts.nodeRecord.path}`);
|
|
832
|
+
if (artifacts.decisionEnvelope.exists)
|
|
833
|
+
refs.push(`decision:${artifacts.decisionEnvelope.path}`);
|
|
834
|
+
if (artifacts.decisionEnvelopeValidation.exists)
|
|
835
|
+
refs.push(`decision-validation:${artifacts.decisionEnvelopeValidation.path}`);
|
|
836
|
+
if (artifacts.humanEscalationJson.exists)
|
|
837
|
+
refs.push(`escalation-json:${artifacts.humanEscalationJson.path}`);
|
|
838
|
+
if (artifacts.humanEscalationMd.exists)
|
|
839
|
+
refs.push(`escalation-md:${artifacts.humanEscalationMd.path}`);
|
|
840
|
+
if (artifacts.humanApproval.exists)
|
|
841
|
+
refs.push(`approval:${artifacts.humanApproval.path}`);
|
|
842
|
+
if (artifacts.humanRejection.exists)
|
|
843
|
+
refs.push(`rejection:${artifacts.humanRejection.path}`);
|
|
844
|
+
return refs.length > 0 ? refs.join(", ") : "-";
|
|
845
|
+
}
|
|
846
|
+
function formatCloseoutVerificationEvidence(run) {
|
|
847
|
+
const verificationNodes = run.nodes.filter((node) => node.executor === "shell" ||
|
|
848
|
+
node.nodeId.includes("verify") ||
|
|
849
|
+
node.nodeId.includes("gate"));
|
|
850
|
+
const targets = verificationNodes.length > 0
|
|
851
|
+
? verificationNodes
|
|
852
|
+
: run.nodes.filter((node) => node.status !== "PENDING");
|
|
853
|
+
if (targets.length === 0) {
|
|
854
|
+
return ["No verification evidence nodes found."];
|
|
855
|
+
}
|
|
856
|
+
return targets.map((node) => {
|
|
857
|
+
const artifacts = formatArtifactFlags(node.artifacts);
|
|
858
|
+
const artifactRefs = formatCloseoutArtifactRefs(node.artifacts);
|
|
859
|
+
const exitHint = node.failureCategory && node.failureCategory !== "success"
|
|
860
|
+
? `exit/failure=${node.failureCategory}`
|
|
861
|
+
: "exit=0";
|
|
862
|
+
return `- **${node.nodeId}** (${node.executor}): status=${node.status}, ${exitHint}, normalized=${node.normalizedFailureCategory ?? "-"}, artifacts=${artifacts}, artifactRefs=${artifactRefs}`;
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function formatCloseoutPrimaryFailureSection(run) {
|
|
866
|
+
const failure = run.primaryFailure;
|
|
867
|
+
if (failure.scope === "node" && failure.nodeId) {
|
|
868
|
+
return [
|
|
869
|
+
`- **Scope**: node`,
|
|
870
|
+
`- **Node**: ${failure.nodeId} (${failure.nodeStatus ?? "unknown"})`,
|
|
871
|
+
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
872
|
+
];
|
|
873
|
+
}
|
|
874
|
+
if (run.primaryRecovery.action === "none" &&
|
|
875
|
+
run.status === "finished" &&
|
|
876
|
+
!failure.failureCategory) {
|
|
877
|
+
return ["No primary failure detected."];
|
|
878
|
+
}
|
|
879
|
+
return [
|
|
880
|
+
`- **Scope**: ${failure.scope}`,
|
|
881
|
+
`- **Category (raw/normalized)**: ${failure.failureCategory ?? "-"} / ${failure.normalizedFailureCategory ?? "-"}`,
|
|
882
|
+
];
|
|
883
|
+
}
|
|
884
|
+
function formatCloseoutRecoverySection(run) {
|
|
885
|
+
const rec = run.primaryRecovery;
|
|
886
|
+
return [
|
|
887
|
+
`- **Action**: \`${rec.action}\``,
|
|
888
|
+
`- **Summary**: ${rec.summary}`,
|
|
889
|
+
`- **Reason**: ${rec.reason}`,
|
|
890
|
+
rec.commandHint ? `- **Command Hint**: ${rec.commandHint}` : undefined,
|
|
891
|
+
].filter((line) => line !== undefined);
|
|
892
|
+
}
|
|
893
|
+
function formatSuggestedReportSection(run) {
|
|
894
|
+
const statusLabel = run.status === "finished" ? "completed successfully" : run.status;
|
|
895
|
+
return [
|
|
896
|
+
`- **Title**: ${run.title} (${run.runId})`,
|
|
897
|
+
`- **Outcome**: Run ${statusLabel} with ${summarizeNodeStatuses(run.nodes)}.`,
|
|
898
|
+
`- **Primary recovery**: ${run.primaryRecovery.summary} [\`${run.primaryRecovery.action}\`]`,
|
|
899
|
+
`- **Canonical facts**: \`dag report --run-id ${run.runId} --lifecycle completed --json\``,
|
|
900
|
+
`- **Run dir**: ${run.runDir}`,
|
|
901
|
+
];
|
|
902
|
+
}
|
|
903
|
+
function formatSuggestedProgressSection(run) {
|
|
904
|
+
return [
|
|
905
|
+
`- Mark milestone complete for run \`${run.runId}\` (${run.title}).`,
|
|
906
|
+
`- Status: ${run.status}; lifecycle: ${run.lifecycle}.`,
|
|
907
|
+
`- Node summary: ${summarizeNodeStatuses(run.nodes)}.`,
|
|
908
|
+
run.finishedAt
|
|
909
|
+
? `- Finished at: ${run.finishedAt}.`
|
|
910
|
+
: "- Finished at: (not recorded).",
|
|
911
|
+
];
|
|
912
|
+
}
|
|
913
|
+
function formatSuggestedHandoffSection(run) {
|
|
914
|
+
return [
|
|
915
|
+
`- Next operator action: ${suggestedNextAction(run)}`,
|
|
916
|
+
`- Review artifacts under \`${run.runDir}\` before publishing docs.`,
|
|
917
|
+
`- Re-validate with \`dag report --run-id ${run.runId} --lifecycle completed --markdown\`.`,
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
function formatRemainingRisksSection(run) {
|
|
921
|
+
const risks = [];
|
|
922
|
+
if (run.status !== "finished") {
|
|
923
|
+
risks.push(`- Run status is \`${run.status}\`; confirm recovery before archiving.`);
|
|
924
|
+
}
|
|
925
|
+
if (run.primaryRecovery.humanRequired) {
|
|
926
|
+
risks.push("- Human input is still required per recovery recommendation.");
|
|
927
|
+
}
|
|
928
|
+
if (run.downstreamSkippedNodes.length > 0) {
|
|
929
|
+
risks.push(`- Downstream skipped nodes: ${run.downstreamSkippedNodes.map((node) => node.nodeId).join(", ")}.`);
|
|
930
|
+
}
|
|
931
|
+
const failedNodes = failureRows(run);
|
|
932
|
+
if (failedNodes.length > 0) {
|
|
933
|
+
risks.push(`- Failed nodes remain in facts: ${failedNodes.map((node) => node.nodeId).join(", ")}.`);
|
|
934
|
+
}
|
|
935
|
+
if (risks.length === 0) {
|
|
936
|
+
risks.push("- No outstanding risks derived from run facts.");
|
|
937
|
+
}
|
|
938
|
+
risks.push("- This draft is advisory; edit before committing to `docs/reports/` or `docs/progress/`.");
|
|
939
|
+
return risks;
|
|
940
|
+
}
|
|
941
|
+
export function formatDagCloseoutDraftMarkdown(run) {
|
|
942
|
+
const lines = [
|
|
943
|
+
`# DAG Closeout Draft: ${run.runId}`,
|
|
944
|
+
"",
|
|
945
|
+
DAG_CLOSEOUT_DRAFT_DISCLAIMER,
|
|
946
|
+
"",
|
|
947
|
+
"## Run Summary",
|
|
948
|
+
`- **Run ID**: ${run.runId}`,
|
|
949
|
+
`- **Title**: ${run.title}`,
|
|
950
|
+
`- **Lifecycle**: ${run.lifecycle}`,
|
|
951
|
+
`- **Status**: ${run.status}`,
|
|
952
|
+
`- **Started**: ${formatTimestamp(run.startedAt)}`,
|
|
953
|
+
`- **Finished**: ${formatTimestamp(run.finishedAt)}`,
|
|
954
|
+
`- **Duration**: ${formatRunDuration(run.startedAt, run.finishedAt)}`,
|
|
955
|
+
`- **Nodes**: ${summarizeNodeStatuses(run.nodes)}`,
|
|
956
|
+
`- **Run Dir**: ${run.runDir}`,
|
|
957
|
+
"",
|
|
958
|
+
"## Primary Failure & Recovery",
|
|
959
|
+
...formatCloseoutPrimaryFailureSection(run),
|
|
960
|
+
"",
|
|
961
|
+
"### Recovery Recommendation",
|
|
962
|
+
...formatCloseoutRecoverySection(run),
|
|
963
|
+
"",
|
|
964
|
+
"## Node Status Summary",
|
|
965
|
+
...formatCloseoutNodeStatusTable(run),
|
|
966
|
+
"",
|
|
967
|
+
"## Verification Evidence",
|
|
968
|
+
...formatCloseoutVerificationEvidence(run),
|
|
969
|
+
"",
|
|
970
|
+
"## Suggested Report Section",
|
|
971
|
+
...formatSuggestedReportSection(run),
|
|
972
|
+
"",
|
|
973
|
+
"## Suggested Progress Section",
|
|
974
|
+
...formatSuggestedProgressSection(run),
|
|
975
|
+
"",
|
|
976
|
+
"## Suggested Handoff Section",
|
|
977
|
+
...formatSuggestedHandoffSection(run),
|
|
978
|
+
"",
|
|
979
|
+
"## Remaining Risks & Next Steps",
|
|
980
|
+
...formatRemainingRisksSection(run),
|
|
981
|
+
"",
|
|
982
|
+
];
|
|
983
|
+
return lines.join("\n");
|
|
984
|
+
}
|
|
985
|
+
export async function buildDagCloseoutDraft(options) {
|
|
986
|
+
if (options.runId) {
|
|
987
|
+
const located = await locateDagRun(options.repoRoot, options.runId);
|
|
988
|
+
if (!located) {
|
|
989
|
+
throw new Error(`dag run not found: ${options.runId}`);
|
|
990
|
+
}
|
|
991
|
+
if (located.lifecycle !== "completed") {
|
|
992
|
+
throw new Error(`closeout draft requires a completed run; found lifecycle=${located.lifecycle} for run ${options.runId}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const report = await buildDagReport({
|
|
996
|
+
repoRoot: options.repoRoot,
|
|
997
|
+
runId: options.runId,
|
|
998
|
+
lifecycle: "completed",
|
|
999
|
+
latest: options.runId === undefined,
|
|
1000
|
+
});
|
|
1001
|
+
if (report.runs.length === 0) {
|
|
1002
|
+
if (options.runId) {
|
|
1003
|
+
throw new Error(`dag run not found: ${options.runId}`);
|
|
1004
|
+
}
|
|
1005
|
+
throw new Error("no completed DAG runs found");
|
|
1006
|
+
}
|
|
1007
|
+
const run = report.runs[0];
|
|
1008
|
+
if (run.lifecycle !== "completed") {
|
|
1009
|
+
throw new Error(`closeout draft requires a completed run; found lifecycle=${run.lifecycle} for run ${run.runId}`);
|
|
1010
|
+
}
|
|
1011
|
+
const markdown = formatDagCloseoutDraftMarkdown(run);
|
|
1012
|
+
const outputPath = resolveCloseoutDraftOutputPath(options.repoRoot, options.outputPath ?? defaultCloseoutDraftOutputPath(run.runId));
|
|
1013
|
+
assertCloseoutDraftOutputPathAllowed(options.repoRoot, outputPath);
|
|
1014
|
+
return {
|
|
1015
|
+
run,
|
|
1016
|
+
markdown,
|
|
1017
|
+
outputPath,
|
|
1018
|
+
};
|
|
1019
|
+
}
|