@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.
Files changed (264) hide show
  1. package/AGENTS.md +121 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +144 -0
  4. package/bin/loop-agent.js +21 -0
  5. package/dist/adapters/aimax.js +91 -0
  6. package/dist/adapters/context.js +32 -0
  7. package/dist/adapters/index.js +28 -0
  8. package/dist/adapters/loop-agent.js +98 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/cli/catalog.js +259 -0
  11. package/dist/cli/help.js +55 -0
  12. package/dist/cli/index.js +3 -0
  13. package/dist/cli/program.js +505 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/closeout.js +13 -0
  16. package/dist/commands/coverage-audit.js +14 -0
  17. package/dist/commands/cursor-prompt.js +222 -0
  18. package/dist/commands/cursor-worker.js +43 -0
  19. package/dist/commands/dag-approve.js +102 -0
  20. package/dist/commands/dag-final-verification.js +76 -0
  21. package/dist/commands/dag-init-hybrid.js +56 -0
  22. package/dist/commands/dag-reconcile-tasks.js +51 -0
  23. package/dist/commands/dag-reject.js +91 -0
  24. package/dist/commands/dag-report.js +177 -0
  25. package/dist/commands/dag-resume.js +34 -0
  26. package/dist/commands/dag-run-task.js +470 -0
  27. package/dist/commands/dag-validate.js +186 -0
  28. package/dist/commands/dag-workflow-compile.js +91 -0
  29. package/dist/commands/dag-workflow-plan.js +130 -0
  30. package/dist/commands/dag-workflow-validate.js +66 -0
  31. package/dist/commands/delegate.js +132 -0
  32. package/dist/commands/docs-archive.js +5 -0
  33. package/dist/commands/docs-audit.js +5 -0
  34. package/dist/commands/doctor.js +50 -0
  35. package/dist/commands/goal.js +92 -0
  36. package/dist/commands/handoff-check.js +5 -0
  37. package/dist/commands/harvest.js +44 -0
  38. package/dist/commands/inspect.js +11 -0
  39. package/dist/commands/instructions.js +195 -0
  40. package/dist/commands/knowledge.js +64 -0
  41. package/dist/commands/loop-benchmark.js +72 -0
  42. package/dist/commands/loop.js +241 -0
  43. package/dist/commands/new-task.js +5 -0
  44. package/dist/commands/pi-prompt.js +181 -0
  45. package/dist/commands/pi-reuse-benchmark.js +153 -0
  46. package/dist/commands/plan-list.js +5 -0
  47. package/dist/commands/promote-run.js +29 -0
  48. package/dist/commands/reference-index.js +16 -0
  49. package/dist/commands/run-dag.js +184 -0
  50. package/dist/commands/spine.js +38 -0
  51. package/dist/commands/stats.js +84 -0
  52. package/dist/commands/status.js +56 -0
  53. package/dist/commands/study-init.js +192 -0
  54. package/dist/commands/workflow.js +259 -0
  55. package/dist/commands/worktree-create.js +31 -0
  56. package/dist/commands/worktree-list.js +5 -0
  57. package/dist/commands/worktree-remove.js +26 -0
  58. package/dist/cursor-worker-entry.js +8 -0
  59. package/dist/executors/config-core.js +55 -0
  60. package/dist/executors/config.js +2 -0
  61. package/dist/executors/cursor-artifacts.js +33 -0
  62. package/dist/executors/cursor-execution-log.js +81 -0
  63. package/dist/executors/cursor-executor-artifacts.js +135 -0
  64. package/dist/executors/cursor-executor.js +468 -0
  65. package/dist/executors/cursor-run.js +115 -0
  66. package/dist/executors/cursor-tool.js +94 -0
  67. package/dist/executors/cursor-worker-client.js +213 -0
  68. package/dist/executors/cursor-worker-protocol.js +18 -0
  69. package/dist/executors/cursor-worker-server.js +54 -0
  70. package/dist/executors/cursor-worker.js +3 -0
  71. package/dist/executors/cursor.js +6 -0
  72. package/dist/executors/dag-cursor-executor.js +88 -0
  73. package/dist/executors/dag-pi-executor.js +322 -0
  74. package/dist/executors/dag-static-executor.js +45 -0
  75. package/dist/executors/dag.js +4 -0
  76. package/dist/executors/index.js +8 -0
  77. package/dist/executors/model-routing.js +60 -0
  78. package/dist/executors/pi-event-serializer.js +43 -0
  79. package/dist/executors/pi-executor.js +606 -0
  80. package/dist/executors/pi-reuse-benchmark.js +316 -0
  81. package/dist/executors/pi-runtime-reuse.js +29 -0
  82. package/dist/executors/pi-sdk-executor.js +255 -0
  83. package/dist/executors/pi-sdk.js +1 -0
  84. package/dist/executors/pi.js +3 -0
  85. package/dist/executors/shell-executor.js +300 -0
  86. package/dist/executors/shell-presets.js +47 -0
  87. package/dist/executors/shell-verification.js +251 -0
  88. package/dist/executors/shell-write-guard.js +126 -0
  89. package/dist/executors/shell.js +3 -0
  90. package/dist/executors/static.js +1 -0
  91. package/dist/governance/checks.js +434 -0
  92. package/dist/governance/harness.js +9 -0
  93. package/dist/governance/index.js +3 -0
  94. package/dist/governance/manifest-types.js +128 -0
  95. package/dist/governance/manifest.js +2 -0
  96. package/dist/governance/path-guard.js +69 -0
  97. package/dist/governance/path-guards.js +2 -0
  98. package/dist/governance/profiles.js +3 -0
  99. package/dist/governance/requirement-coverage.js +425 -0
  100. package/dist/governance/skill-safety.js +135 -0
  101. package/dist/governance/spine-audit.js +152 -0
  102. package/dist/records/closeout.js +2 -0
  103. package/dist/records/harvest.js +236 -0
  104. package/dist/records/index.js +3 -0
  105. package/dist/records/one-shot-runs.js +421 -0
  106. package/dist/records/promotion.js +199 -0
  107. package/dist/shared/artifacts-core.js +88 -0
  108. package/dist/shared/artifacts.js +2 -0
  109. package/dist/shared/context-files.js +32 -0
  110. package/dist/shared/context.js +2 -0
  111. package/dist/shared/copy-dir.js +17 -0
  112. package/dist/shared/git-progress.js +165 -0
  113. package/dist/shared/index.js +5 -0
  114. package/dist/shared/logger.js +23 -0
  115. package/dist/shared/one-shot-prompt-args.js +98 -0
  116. package/dist/shared/path-refs.js +31 -0
  117. package/dist/shared/prompts.js +26 -0
  118. package/dist/shared/reference-context.js +238 -0
  119. package/dist/shared/timeout-policy.js +19 -0
  120. package/dist/shared/timeout.js +1 -0
  121. package/dist/shared/types.js +5 -0
  122. package/dist/task/config-types.js +97 -0
  123. package/dist/task/config.js +2 -0
  124. package/dist/task/delegate.js +220 -0
  125. package/dist/task/goal-audit.js +51 -0
  126. package/dist/task/goal-policy.js +8 -0
  127. package/dist/task/goal.js +3 -0
  128. package/dist/task/ids.js +1 -0
  129. package/dist/task/index.js +9 -0
  130. package/dist/task/lifecycle.js +1 -0
  131. package/dist/task/paths.js +1 -0
  132. package/dist/task/read-model.js +149 -0
  133. package/dist/task/runtime.js +699 -0
  134. package/dist/task/source-state.js +1 -0
  135. package/dist/task/state.js +55 -0
  136. package/dist/task/subagent-guidance.js +1 -0
  137. package/dist/task/workflow-state-types.js +92 -0
  138. package/dist/task/worktree-cleanup.js +140 -0
  139. package/dist/task/worktree.js +171 -0
  140. package/dist/workflows/dag/authoring.js +8 -0
  141. package/dist/workflows/dag/authority-surface.js +138 -0
  142. package/dist/workflows/dag/canvas-observer.js +474 -0
  143. package/dist/workflows/dag/decision-envelope.js +502 -0
  144. package/dist/workflows/dag/decision-evidence.js +153 -0
  145. package/dist/workflows/dag/decision-gates.js +1 -0
  146. package/dist/workflows/dag/executor-registry.js +25 -0
  147. package/dist/workflows/dag/facts.js +4 -0
  148. package/dist/workflows/dag/failure-category.js +111 -0
  149. package/dist/workflows/dag/final-verification.js +180 -0
  150. package/dist/workflows/dag/governance-constants.js +5 -0
  151. package/dist/workflows/dag/governance-profile.js +405 -0
  152. package/dist/workflows/dag/index.js +6 -0
  153. package/dist/workflows/dag/init-hybrid.js +855 -0
  154. package/dist/workflows/dag/knowledge-curator.js +162 -0
  155. package/dist/workflows/dag/lifecycle.js +484 -0
  156. package/dist/workflows/dag/prompt-source.js +88 -0
  157. package/dist/workflows/dag/prompt.js +130 -0
  158. package/dist/workflows/dag/reconcile-tasks.js +404 -0
  159. package/dist/workflows/dag/recovery-recommendation.js +226 -0
  160. package/dist/workflows/dag/repair-artifact.js +136 -0
  161. package/dist/workflows/dag/report.js +1019 -0
  162. package/dist/workflows/dag/runner.js +1677 -0
  163. package/dist/workflows/dag/runtime.js +5 -0
  164. package/dist/workflows/dag/skill-instructions.js +471 -0
  165. package/dist/workflows/dag/skills.js +41 -0
  166. package/dist/workflows/dag/spec.js +3 -0
  167. package/dist/workflows/dag/topo.js +30 -0
  168. package/dist/workflows/dag/types.js +275 -0
  169. package/dist/workflows/dag/upstream-artifacts.js +95 -0
  170. package/dist/workflows/dag/validate.js +527 -0
  171. package/dist/workflows/dynamic/artifacts.js +65 -0
  172. package/dist/workflows/dynamic/compile.js +360 -0
  173. package/dist/workflows/dynamic/compileTypes.js +1 -0
  174. package/dist/workflows/dynamic/errors.js +5 -0
  175. package/dist/workflows/dynamic/index.js +7 -0
  176. package/dist/workflows/dynamic/profiles.js +156 -0
  177. package/dist/workflows/dynamic/spec.js +114 -0
  178. package/dist/workflows/dynamic/validate.js +275 -0
  179. package/dist/workflows/loop/actions.js +1334 -0
  180. package/dist/workflows/loop/benchmark.js +510 -0
  181. package/dist/workflows/loop/closeout.js +134 -0
  182. package/dist/workflows/loop/context.js +48 -0
  183. package/dist/workflows/loop/events.js +25 -0
  184. package/dist/workflows/loop/hash.js +32 -0
  185. package/dist/workflows/loop/index.js +8 -0
  186. package/dist/workflows/loop/paths.js +17 -0
  187. package/dist/workflows/loop/rounds.js +81 -0
  188. package/dist/workflows/loop/signals.js +55 -0
  189. package/dist/workflows/loop/state.js +116 -0
  190. package/dist/workflows/loop/templates.js +54 -0
  191. package/dist/workflows/loop/types.js +28 -0
  192. package/docs/README.md +62 -0
  193. package/docs/agent-dag-recovery-playbook.md +158 -0
  194. package/docs/agent-dag-runner.md +40 -0
  195. package/docs/cursor-executor-usage.md +25 -0
  196. package/docs/decisions/README.md +3 -0
  197. package/docs/design/README.md +36 -0
  198. package/docs/development-principles.md +71 -0
  199. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
  200. package/docs/exec-plans/README.md +6 -0
  201. package/docs/exec-plans/active/README.md +5 -0
  202. package/docs/exec-plans/completed/README.md +5 -0
  203. package/docs/feature-workflow.md +184 -0
  204. package/docs/harness-methodology-debugging.md +153 -0
  205. package/docs/harness-methodology-tdd.md +130 -0
  206. package/docs/harness-methodology-verification.md +27 -0
  207. package/docs/loop-agent-harness.md +42 -0
  208. package/docs/progress/README.md +3 -0
  209. package/docs/reports/README.md +3 -0
  210. package/docs/templates/adr.md +60 -0
  211. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
  212. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
  213. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
  214. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
  215. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
  216. package/docs/templates/agent-dag-report.schema.json +423 -0
  217. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
  218. package/docs/templates/agent-dag.base.json +195 -0
  219. package/docs/templates/agent-dag.final-verification.json +190 -0
  220. package/docs/templates/agent-dag.schema.json +316 -0
  221. package/docs/templates/agent-dag.supervised-implementation.json +500 -0
  222. package/docs/templates/exec-plan.md +64 -0
  223. package/docs/templates/feature-spec.md +53 -0
  224. package/docs/templates/hybrid-dag.json +193 -0
  225. package/docs/templates/progress-log.md +17 -0
  226. package/docs/templates/project-start-checklist.md +9 -0
  227. package/docs/templates/qa-report.md +42 -0
  228. package/docs/templates/sprint-contract.md +29 -0
  229. package/docs/verification-matrix.md +30 -0
  230. package/examples/decision-gate-agent-dag.json +123 -0
  231. package/examples/example-dag.json +51 -0
  232. package/examples/hybrid-loop-agent-dag.json +194 -0
  233. package/harness.json +92 -0
  234. package/package.json +61 -0
  235. package/skills/ai-engineering-context/SKILL.md +48 -0
  236. package/skills/loop-agent/SKILL.md +260 -0
  237. package/skills/loop-agent/references/README.md +63 -0
  238. package/skills/loop-agent/references/command-reference.md +315 -0
  239. package/skills/loop-agent/references/harness-policy.md +258 -0
  240. package/skills/loop-agent/references/hybrid-dag.md +216 -0
  241. package/skills/loop-agent/references/learned/README.md +21 -0
  242. package/skills/loop-agent/references/model-routing.md +36 -0
  243. package/skills/loop-agent/references/multi-worktree.md +54 -0
  244. package/skills/loop-agent/references/one-shot-runs.md +85 -0
  245. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
  246. package/skills/loop-agent/references/pi-prompt.md +23 -0
  247. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
  248. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
  249. package/skills/loop-agent/references/task-workflow.md +84 -0
  250. package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
  251. package/skills/requesting-code-review/SKILL.md +101 -0
  252. package/skills/requesting-code-review/code-reviewer.md +168 -0
  253. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  254. package/skills/systematic-debugging/SKILL.md +296 -0
  255. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  256. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  257. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  258. package/skills/systematic-debugging/find-polluter.sh +63 -0
  259. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  260. package/skills/systematic-debugging/test-academic.md +14 -0
  261. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  262. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  263. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  264. package/skills/verification-before-completion/SKILL.md +154 -0
@@ -0,0 +1,162 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { auditSkillMarkdownContent } from "../../governance/skill-safety.js";
4
+ import { repairArtifactSchema } from "./repair-artifact.js";
5
+ async function exists(filePath) {
6
+ try {
7
+ await access(filePath);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function normalizeScopeShape(fixScope) {
15
+ if (fixScope.length === 0)
16
+ return "none";
17
+ return fixScope
18
+ .map((scope) => scope.replace(/[^/]+$/g, "*"))
19
+ .sort()
20
+ .join(", ");
21
+ }
22
+ function parseLegacyArtifact(raw) {
23
+ // Preserve completed-run learning from pre-REPAIR_ARTIFACT_JSON supervisor output.
24
+ const block = raw.supervisorStructuredBlock;
25
+ if (!block)
26
+ return undefined;
27
+ const failureClass = block.FAILURE_CLASS ?? "unknown";
28
+ const parsedFailureClass = repairArtifactSchema.shape.failureClass.safeParse(failureClass);
29
+ if (!parsedFailureClass.success)
30
+ return undefined;
31
+ return {
32
+ schemaVersion: 1,
33
+ verdict: "request-revision",
34
+ failureClass: parsedFailureClass.data,
35
+ rootCause: block.ROOT_CAUSE ?? "Legacy supervisor block did not record rootCause.",
36
+ fixScope: block.FIX_SCOPE ? block.FIX_SCOPE.split(",").map((item) => item.trim()).filter(Boolean) : [],
37
+ invariant: block.INVARIANT ?? "Preserve verified behavior.",
38
+ evidenceRefs: raw.runId ? [raw.runId] : [],
39
+ rawLogFallbackAllowed: true,
40
+ };
41
+ }
42
+ function extractRepairArtifact(raw) {
43
+ const parsed = repairArtifactSchema.safeParse(raw.repairArtifact);
44
+ if (parsed.success)
45
+ return parsed.data;
46
+ return parseLegacyArtifact(raw);
47
+ }
48
+ function addPattern(patterns, runId, artifact) {
49
+ const fixScopeShape = normalizeScopeShape(artifact.fixScope);
50
+ const key = [
51
+ artifact.failureClass,
52
+ fixScopeShape,
53
+ artifact.invariant.toLowerCase(),
54
+ ].join("|");
55
+ const existing = patterns.get(key);
56
+ if (existing) {
57
+ existing.count += 1;
58
+ if (!existing.sourceRunIds.includes(runId))
59
+ existing.sourceRunIds.push(runId);
60
+ for (const ref of artifact.evidenceRefs) {
61
+ if (!existing.evidenceRefs.includes(ref))
62
+ existing.evidenceRefs.push(ref);
63
+ }
64
+ if (!existing.rootCauses.includes(artifact.rootCause)) {
65
+ existing.rootCauses.push(artifact.rootCause);
66
+ }
67
+ return;
68
+ }
69
+ patterns.set(key, {
70
+ key,
71
+ failureClass: artifact.failureClass,
72
+ fixScopeShape,
73
+ invariant: artifact.invariant,
74
+ sourceRunIds: [runId],
75
+ evidenceRefs: [...artifact.evidenceRefs],
76
+ rootCauses: [artifact.rootCause],
77
+ count: 1,
78
+ });
79
+ }
80
+ export function formatKnowledgeProposalMarkdown(patterns) {
81
+ const lines = [
82
+ "# loop-agent Learned Guidance Proposal",
83
+ "",
84
+ "Status: human-gated proposal",
85
+ "",
86
+ "This proposal is generated from completed convergence patterns. It is advisory process guidance only; it does not modify accepted learned skill files.",
87
+ "",
88
+ "## Candidate Patterns",
89
+ "",
90
+ ];
91
+ for (const pattern of patterns) {
92
+ lines.push(`### ${pattern.failureClass} / ${pattern.fixScopeShape}`, "", `- count: ${pattern.count}`, `- source runs: ${pattern.sourceRunIds.join(", ")}`, `- invariant: ${pattern.invariant}`, `- root causes: ${pattern.rootCauses.join("; ")}`, `- evidence refs: ${pattern.evidenceRefs.length > 0 ? pattern.evidenceRefs.join(", ") : "(none)"}`, "- when to apply: use when the failure class, bounded fix scope, and invariant match a new supervised DAG repair round.", "- when not to apply: do not apply when the task has different ownership boundaries, missing full verification, or higher-risk public contract changes.", "- safety notes: keep guidance Markdown-only, advisory, and subordinate to writeSet governance and final shell verification.", "");
93
+ }
94
+ if (patterns.length === 0) {
95
+ lines.push("No accepted convergence patterns were found.", "");
96
+ }
97
+ return `${lines.join("\n")}\n`;
98
+ }
99
+ export async function curateKnowledgePatterns(input) {
100
+ const patternsPath = path.join(input.repoRoot, ".harness", "knowledge", "patterns.jsonl");
101
+ if (!(await exists(patternsPath))) {
102
+ return {
103
+ ok: true,
104
+ patternsPath,
105
+ patternCount: 0,
106
+ safetyFindings: [],
107
+ message: "no patterns.jsonl found; no proposal generated",
108
+ };
109
+ }
110
+ const raw = await readFile(patternsPath, "utf-8");
111
+ const grouped = new Map();
112
+ for (const line of raw.split(/\r?\n/)) {
113
+ if (!line.trim())
114
+ continue;
115
+ const parsed = JSON.parse(line);
116
+ if (parsed.type !== "dag-convergence-repair")
117
+ continue;
118
+ if (parsed.terminalReason && parsed.terminalReason !== "hard-verify-pass")
119
+ continue;
120
+ const artifact = extractRepairArtifact(parsed);
121
+ if (!artifact || artifact.verdict !== "request-revision")
122
+ continue;
123
+ addPattern(grouped, parsed.runId ?? "unknown-run", artifact);
124
+ }
125
+ const patterns = Array.from(grouped.values()).sort((a, b) => a.key.localeCompare(b.key));
126
+ const proposalMarkdown = formatKnowledgeProposalMarkdown(patterns);
127
+ const outputPath = input.outputPath
128
+ ? path.resolve(input.repoRoot, input.outputPath)
129
+ : undefined;
130
+ const safetyFindings = auditSkillMarkdownContent({
131
+ repoRoot: input.repoRoot,
132
+ filePath: outputPath ?? path.join(input.repoRoot, ".harness", "knowledge", "learned-proposal.md"),
133
+ content: proposalMarkdown,
134
+ profile: "learned",
135
+ });
136
+ if (safetyFindings.length > 0) {
137
+ return {
138
+ ok: false,
139
+ patternsPath,
140
+ outputPath,
141
+ patternCount: patterns.length,
142
+ proposalMarkdown,
143
+ safetyFindings,
144
+ message: "proposal failed skill safety audit",
145
+ };
146
+ }
147
+ if (outputPath) {
148
+ await mkdir(path.dirname(outputPath), { recursive: true });
149
+ await writeFile(outputPath, proposalMarkdown, "utf-8");
150
+ }
151
+ return {
152
+ ok: true,
153
+ patternsPath,
154
+ outputPath,
155
+ patternCount: patterns.length,
156
+ proposalMarkdown,
157
+ safetyFindings,
158
+ message: patterns.length === 0
159
+ ? "patterns found but none qualified for proposal"
160
+ : "proposal generated",
161
+ };
162
+ }
@@ -0,0 +1,484 @@
1
+ import { access, mkdir, readFile, readdir, rename, writeFile, } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parseDagSpec } from "./types.js";
4
+ const DAG_LIFECYCLE_SCAN_ORDER = [
5
+ "paused",
6
+ "active",
7
+ "completed",
8
+ ];
9
+ const PAUSED_APPROVAL_FLOW_HINT = "Use dag approve / dag reject, then dag resume when approved.";
10
+ export const DAG_RUNS_DIR = path.join(".harness", "dag-runs");
11
+ export function getDagRunDir(cwd, lifecycle, runId) {
12
+ return path.join(cwd, DAG_RUNS_DIR, lifecycle, runId);
13
+ }
14
+ export async function dagRunDirExists(runDir) {
15
+ try {
16
+ await access(runDir);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export async function readDagRunState(runDir) {
24
+ const raw = JSON.parse(await readFile(path.join(runDir, "state.json"), "utf-8"));
25
+ return raw;
26
+ }
27
+ export async function writeDagRunState(runDir, state) {
28
+ await writeFile(path.join(runDir, "state.json"), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
29
+ }
30
+ export async function readDagRunSpec(runDir) {
31
+ const raw = JSON.parse(await readFile(path.join(runDir, "run.json"), "utf-8"));
32
+ return parseDagSpec(raw);
33
+ }
34
+ export async function locateDagRun(cwd, runId) {
35
+ for (const lifecycle of ["paused", "active", "completed"]) {
36
+ const runDir = getDagRunDir(cwd, lifecycle, runId);
37
+ if (await dagRunDirExists(runDir)) {
38
+ return { lifecycle, runDir };
39
+ }
40
+ }
41
+ return undefined;
42
+ }
43
+ export async function explainPausedRunLookupFailure(cwd, runId) {
44
+ const located = await locateDagRun(cwd, runId);
45
+ if (!located) {
46
+ return `paused dag run not found: ${runId}`;
47
+ }
48
+ if (located.lifecycle === "completed") {
49
+ let detail = `dag run ${runId} is already completed`;
50
+ try {
51
+ const state = await readDagRunState(located.runDir);
52
+ detail += ` (status=${state.status}`;
53
+ if (state.failureCategory) {
54
+ detail += `, failureCategory=${state.failureCategory}`;
55
+ }
56
+ detail += ")";
57
+ }
58
+ catch {
59
+ // keep base message
60
+ }
61
+ return `${detail}; approve/reject require a paused run`;
62
+ }
63
+ if (located.lifecycle === "active") {
64
+ try {
65
+ const state = await readDagRunState(located.runDir);
66
+ if (state.status === "running" && state.humanDecisionNodeId) {
67
+ return `dag run ${runId} is active and awaiting dag resume (already approved)`;
68
+ }
69
+ return `dag run ${runId} is active (status=${state.status}); approve/reject require paused lifecycle`;
70
+ }
71
+ catch {
72
+ return `dag run ${runId} is active but state.json is unreadable`;
73
+ }
74
+ }
75
+ return `paused dag run not found: ${runId}`;
76
+ }
77
+ export async function requirePausedDagRun(cwd, runId) {
78
+ const runDir = getDagRunDir(cwd, "paused", runId);
79
+ if (!(await dagRunDirExists(runDir))) {
80
+ throw new Error(await explainPausedRunLookupFailure(cwd, runId));
81
+ }
82
+ const state = await readDagRunState(runDir);
83
+ if (state.runId !== runId) {
84
+ throw new Error(`run-id mismatch in paused run: expected ${runId}, got ${state.runId}`);
85
+ }
86
+ if (state.status !== "paused") {
87
+ throw new Error(`dag run ${runId} is not paused (status=${state.status})`);
88
+ }
89
+ if (!state.pausedByNodeId) {
90
+ throw new Error(`dag run ${runId} is paused but missing pausedByNodeId`);
91
+ }
92
+ const spec = await readDagRunSpec(runDir);
93
+ return { runDir, state, spec };
94
+ }
95
+ export async function requireActiveDagRun(cwd, runId) {
96
+ const runDir = getDagRunDir(cwd, "active", runId);
97
+ if (!(await dagRunDirExists(runDir))) {
98
+ throw new Error(`active dag run not found: ${runId}`);
99
+ }
100
+ const state = await readDagRunState(runDir);
101
+ if (state.runId !== runId) {
102
+ throw new Error(`run-id mismatch in active run: expected ${runId}, got ${state.runId}`);
103
+ }
104
+ const spec = await readDagRunSpec(runDir);
105
+ return { runDir, state, spec };
106
+ }
107
+ export async function assertDagRunTransferTargetAvailable(targetRunDir) {
108
+ try {
109
+ await access(targetRunDir);
110
+ }
111
+ catch {
112
+ return;
113
+ }
114
+ throw new Error(`refusing to transfer dag run because target already exists: ${targetRunDir}`);
115
+ }
116
+ export async function transferDagRunDir(sourceRunDir, targetRunDir) {
117
+ await mkdir(path.dirname(targetRunDir), { recursive: true });
118
+ await assertDagRunTransferTargetAvailable(targetRunDir);
119
+ await rename(sourceRunDir, targetRunDir);
120
+ return targetRunDir;
121
+ }
122
+ export async function readHumanEscalation(runDir, nodeId) {
123
+ const escalationPath = path.join(runDir, nodeId, "human-escalation.json");
124
+ const raw = JSON.parse(await readFile(escalationPath, "utf-8"));
125
+ if (!raw.humanEscalation?.options?.length) {
126
+ throw new Error(`missing human escalation options for node ${nodeId}`);
127
+ }
128
+ return raw;
129
+ }
130
+ export async function writeHumanApprovalArtifact(input) {
131
+ const nodeDir = path.join(input.runDir, input.nodeId);
132
+ await mkdir(nodeDir, { recursive: true });
133
+ const artifactPath = path.join(nodeDir, "human-approval.json");
134
+ await writeFile(artifactPath, `${JSON.stringify(input.artifact, null, 2)}\n`, "utf-8");
135
+ return artifactPath;
136
+ }
137
+ export async function writeHumanRejectionArtifact(input) {
138
+ const nodeDir = path.join(input.runDir, input.nodeId);
139
+ await mkdir(nodeDir, { recursive: true });
140
+ const artifactPath = path.join(nodeDir, "human-rejection.json");
141
+ await writeFile(artifactPath, `${JSON.stringify(input.artifact, null, 2)}\n`, "utf-8");
142
+ return artifactPath;
143
+ }
144
+ export async function readHumanApprovalArtifact(runDir, nodeId) {
145
+ const artifactPath = path.join(runDir, nodeId, "human-approval.json");
146
+ const raw = JSON.parse(await readFile(artifactPath, "utf-8"));
147
+ return raw;
148
+ }
149
+ export function humanApprovalExists(runDir, nodeId) {
150
+ return path.join(runDir, nodeId, "human-approval.json");
151
+ }
152
+ export async function humanApprovalArtifactExists(runDir, nodeId) {
153
+ try {
154
+ await access(humanApprovalExists(runDir, nodeId));
155
+ return true;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ }
161
+ export const TERMINAL_RUN_STATUSES = new Set([
162
+ "finished",
163
+ "failed",
164
+ "partial_failed",
165
+ ]);
166
+ export function isTerminalDagRunStatus(status) {
167
+ return TERMINAL_RUN_STATUSES.has(status);
168
+ }
169
+ function listPendingNodeIds(state) {
170
+ return Object.values(state.nodes)
171
+ .filter((node) => node.status === "PENDING")
172
+ .map((node) => node.id);
173
+ }
174
+ function listFinishedNodeIds(state) {
175
+ return Object.values(state.nodes)
176
+ .filter((node) => node.status === "FINISHED")
177
+ .map((node) => node.id);
178
+ }
179
+ export function deriveOperatorNextAction(input) {
180
+ if (input.healthIssues.some((issue) => issue.severity === "error")) {
181
+ return "Inspect health issues with dag doctor before approve/reject/resume.";
182
+ }
183
+ if (input.lifecycle === "paused" && input.state.status === "paused") {
184
+ return PAUSED_APPROVAL_FLOW_HINT;
185
+ }
186
+ if (input.lifecycle === "active" &&
187
+ input.state.status === "running" &&
188
+ input.state.humanDecisionNodeId) {
189
+ return input.hasHumanApproval
190
+ ? "Run is approved; use dag resume to continue."
191
+ : "Missing human-approval.json; do not resume until approval artifacts are restored.";
192
+ }
193
+ if (input.lifecycle === "active" &&
194
+ input.state.status === "running" &&
195
+ !input.state.humanDecisionNodeId) {
196
+ return "Monitor in-progress active run; no approve/resume action yet.";
197
+ }
198
+ if (input.lifecycle === "active" &&
199
+ isTerminalDagRunStatus(input.state.status)) {
200
+ return "Stale active directory detected; review harness playbook and archive or remove after inspection.";
201
+ }
202
+ if (input.lifecycle === "completed") {
203
+ if (input.state.failureCategory === "human-rejected") {
204
+ return "Run was human-rejected; revise contract and start a new run.";
205
+ }
206
+ if (input.state.status === "finished") {
207
+ return "Run completed; no lifecycle action required.";
208
+ }
209
+ return "Inspect completed run artifacts before any retry.";
210
+ }
211
+ return "Inspect run facts with dag status and dag report --json.";
212
+ }
213
+ export async function detectDagRunHealthIssues(input) {
214
+ const issues = [];
215
+ const { runId, lifecycle, runDir, state } = input;
216
+ if (state.runId !== runId) {
217
+ issues.push({
218
+ code: "run-id-mismatch",
219
+ severity: "error",
220
+ message: `state.runId=${state.runId} does not match directory runId=${runId}`,
221
+ advisoryAction: "Inspect state.json and directory naming; do not auto-mutate completed facts.",
222
+ });
223
+ }
224
+ if (lifecycle === "active" && isTerminalDagRunStatus(state.status)) {
225
+ issues.push({
226
+ code: "terminal-in-active",
227
+ severity: "warning",
228
+ message: `Run has terminal status=${state.status} but remains under active/`,
229
+ advisoryAction: "Review and archive to completed/ or remove stale directory per harness playbook.",
230
+ });
231
+ }
232
+ if (lifecycle === "active" && state.status === "paused") {
233
+ issues.push({
234
+ code: "paused-in-active",
235
+ severity: "warning",
236
+ message: "Run status=paused but directory is active/; expected paused/",
237
+ advisoryAction: "Inspect with dag doctor; approve/reject/resume may fail until facts are consistent.",
238
+ });
239
+ }
240
+ if (lifecycle === "paused" && state.status !== "paused") {
241
+ issues.push({
242
+ code: "lifecycle-status-mismatch",
243
+ severity: "warning",
244
+ message: `Run is under paused/ but status=${state.status}`,
245
+ advisoryAction: "Use dag doctor; approve/reject/resume may fail until lifecycle facts are consistent.",
246
+ });
247
+ }
248
+ if (lifecycle === "active" &&
249
+ state.status === "running" &&
250
+ state.humanDecisionNodeId) {
251
+ const hasApproval = await humanApprovalArtifactExists(runDir, state.humanDecisionNodeId);
252
+ if (!hasApproval) {
253
+ issues.push({
254
+ code: "missing-approval-artifact",
255
+ severity: "error",
256
+ message: `humanDecisionNodeId=${state.humanDecisionNodeId} but human-approval.json is missing`,
257
+ advisoryAction: "Do not resume; inspect node artifacts and re-approve if needed.",
258
+ });
259
+ }
260
+ }
261
+ if (lifecycle === "completed" &&
262
+ (state.status === "running" || state.status === "paused")) {
263
+ issues.push({
264
+ code: "non-terminal-in-completed",
265
+ severity: "warning",
266
+ message: `Run is under completed/ but status=${state.status}`,
267
+ advisoryAction: "Historical facts may be inconsistent; treat as manual-review only.",
268
+ });
269
+ }
270
+ return issues;
271
+ }
272
+ async function listRunsInLifecycleDir(repoRoot, lifecycle) {
273
+ const lifecycleDir = getDagRunDir(repoRoot, lifecycle, "");
274
+ try {
275
+ await access(lifecycleDir);
276
+ }
277
+ catch {
278
+ return [];
279
+ }
280
+ const entries = await readdir(lifecycleDir, { withFileTypes: true });
281
+ const located = [];
282
+ for (const entry of entries) {
283
+ if (!entry.isDirectory() || entry.name === ".gitkeep")
284
+ continue;
285
+ const runDir = path.join(lifecycleDir, entry.name);
286
+ try {
287
+ await access(path.join(runDir, "state.json"));
288
+ }
289
+ catch {
290
+ located.push({
291
+ runId: entry.name,
292
+ lifecycle,
293
+ runDir,
294
+ });
295
+ continue;
296
+ }
297
+ located.push({ runId: entry.name, lifecycle, runDir });
298
+ }
299
+ return located;
300
+ }
301
+ export async function listAllDagRunEntries(repoRoot) {
302
+ const located = [];
303
+ for (const lifecycle of DAG_LIFECYCLE_SCAN_ORDER) {
304
+ located.push(...(await listRunsInLifecycleDir(repoRoot, lifecycle)));
305
+ }
306
+ return located.sort((left, right) => {
307
+ const lifecycleCompare = DAG_LIFECYCLE_SCAN_ORDER.indexOf(left.lifecycle)
308
+ - DAG_LIFECYCLE_SCAN_ORDER.indexOf(right.lifecycle);
309
+ if (lifecycleCompare !== 0)
310
+ return lifecycleCompare;
311
+ return left.runId.localeCompare(right.runId);
312
+ });
313
+ }
314
+ export async function buildDagOperatorRunSummary(entry) {
315
+ let state;
316
+ try {
317
+ state = await readDagRunState(entry.runDir);
318
+ }
319
+ catch {
320
+ const healthIssues = [
321
+ {
322
+ code: "missing-state-json",
323
+ severity: "error",
324
+ message: `Run directory ${entry.runId} is missing readable state.json`,
325
+ advisoryAction: "Inspect directory contents; do not approve/resume until state facts exist.",
326
+ },
327
+ ];
328
+ return {
329
+ runId: entry.runId,
330
+ title: entry.runId,
331
+ lifecycle: entry.lifecycle,
332
+ status: "pending",
333
+ pausedByNodeId: null,
334
+ pauseReason: null,
335
+ failureCategory: null,
336
+ humanDecisionNodeId: null,
337
+ pendingNodes: [],
338
+ finishedNodes: [],
339
+ healthIssues,
340
+ nextRecommendedAction: deriveOperatorNextAction({
341
+ lifecycle: entry.lifecycle,
342
+ state: {
343
+ version: 1,
344
+ title: entry.runId,
345
+ runId: entry.runId,
346
+ cwd: "",
347
+ startedAt: "",
348
+ status: "pending",
349
+ ranks: [],
350
+ nodes: {},
351
+ },
352
+ hasHumanApproval: false,
353
+ healthIssues,
354
+ }),
355
+ };
356
+ }
357
+ const healthIssues = await detectDagRunHealthIssues({
358
+ runId: entry.runId,
359
+ lifecycle: entry.lifecycle,
360
+ runDir: entry.runDir,
361
+ state,
362
+ });
363
+ const hasHumanApproval = state.humanDecisionNodeId
364
+ ? await humanApprovalArtifactExists(entry.runDir, state.humanDecisionNodeId)
365
+ : false;
366
+ return {
367
+ runId: state.runId,
368
+ title: state.title,
369
+ lifecycle: entry.lifecycle,
370
+ status: state.status,
371
+ startedAt: state.startedAt,
372
+ finishedAt: state.finishedAt,
373
+ pausedByNodeId: state.pausedByNodeId ?? null,
374
+ pauseReason: state.pauseReason ?? null,
375
+ failureCategory: state.failureCategory ?? null,
376
+ humanDecisionNodeId: state.humanDecisionNodeId ?? null,
377
+ pendingNodes: listPendingNodeIds(state),
378
+ finishedNodes: listFinishedNodeIds(state),
379
+ healthIssues,
380
+ nextRecommendedAction: deriveOperatorNextAction({
381
+ lifecycle: entry.lifecycle,
382
+ state,
383
+ hasHumanApproval,
384
+ healthIssues,
385
+ }),
386
+ approvalFlow: entry.lifecycle === "paused" && state.status === "paused"
387
+ ? PAUSED_APPROVAL_FLOW_HINT
388
+ : undefined,
389
+ };
390
+ }
391
+ export async function buildDagStatusReport(repoRoot, runId) {
392
+ const located = await locateDagRun(repoRoot, runId);
393
+ if (!located) {
394
+ throw new Error(`dag run not found: ${runId}`);
395
+ }
396
+ const summary = await buildDagOperatorRunSummary({ ...located, runId });
397
+ const hasHumanApproval = summary.humanDecisionNodeId
398
+ ? await humanApprovalArtifactExists(located.runDir, summary.humanDecisionNodeId)
399
+ : false;
400
+ return {
401
+ ...summary,
402
+ runDir: located.runDir,
403
+ escalationArtifactPath: summary.pausedByNodeId
404
+ ? path.join(located.runDir, summary.pausedByNodeId, "human-escalation.json")
405
+ : null,
406
+ hasHumanApproval,
407
+ };
408
+ }
409
+ function collectDoctorRecommendedActions(runs) {
410
+ const actions = new Set();
411
+ for (const run of runs) {
412
+ if (run.nextRecommendedAction) {
413
+ actions.add(run.nextRecommendedAction);
414
+ }
415
+ for (const issue of run.healthIssues) {
416
+ actions.add(issue.advisoryAction);
417
+ }
418
+ }
419
+ if (actions.size === 0) {
420
+ actions.add("No DAG runtime entries found under .harness/dag-runs/.");
421
+ }
422
+ return [...actions];
423
+ }
424
+ export async function buildDagDoctorReport(repoRoot) {
425
+ const entries = await listAllDagRunEntries(repoRoot);
426
+ const runs = await Promise.all(entries.map(buildDagOperatorRunSummary));
427
+ const healthIssueCount = runs.reduce((count, run) => count + run.healthIssues.length, 0);
428
+ return {
429
+ schemaVersion: 1,
430
+ summary: {
431
+ active: runs.filter((run) => run.lifecycle === "active").length,
432
+ paused: runs.filter((run) => run.lifecycle === "paused").length,
433
+ completed: runs.filter((run) => run.lifecycle === "completed").length,
434
+ healthIssueCount,
435
+ },
436
+ runs,
437
+ recommendedActions: collectDoctorRecommendedActions(runs),
438
+ advisoryOnly: true,
439
+ };
440
+ }
441
+ export function parseDagStatusArgs(args) {
442
+ if (args.length === 0) {
443
+ throw new Error("usage: dag status --run-id <id>");
444
+ }
445
+ let runId;
446
+ for (let i = 0; i < args.length; i += 1) {
447
+ const arg = args[i];
448
+ if (arg === "--run-id") {
449
+ runId = args[++i];
450
+ continue;
451
+ }
452
+ if (arg.startsWith("--run-id=")) {
453
+ runId = arg.slice("--run-id=".length);
454
+ continue;
455
+ }
456
+ if (arg.startsWith("-")) {
457
+ throw new Error(`unknown dag status flag: ${arg}`);
458
+ }
459
+ throw new Error(`unexpected positional argument: ${arg}`);
460
+ }
461
+ if (!runId) {
462
+ throw new Error("dag status requires --run-id <id>");
463
+ }
464
+ return { runId };
465
+ }
466
+ export async function runDagStatus(repoRoot, rawArgs) {
467
+ const parsed = parseDagStatusArgs(rawArgs);
468
+ const report = await buildDagStatusReport(repoRoot, parsed.runId);
469
+ console.log(JSON.stringify(report, null, 2));
470
+ }
471
+ export function parseDagDoctorArgs(args) {
472
+ for (const arg of args) {
473
+ if (arg.startsWith("-")) {
474
+ throw new Error(`unknown dag doctor flag: ${arg}`);
475
+ }
476
+ throw new Error(`unexpected positional argument: ${arg}`);
477
+ }
478
+ return {};
479
+ }
480
+ export async function runDagDoctor(repoRoot, rawArgs) {
481
+ parseDagDoctorArgs(rawArgs);
482
+ const report = await buildDagDoctorReport(repoRoot);
483
+ console.log(JSON.stringify(report, null, 2));
484
+ }