@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,25 @@
1
+ import { appendFile, readFile } from "node:fs/promises";
2
+ import { getLoopPaths } from "./paths.js";
3
+ export async function appendLoopEvent(repoRoot, taskId, input) {
4
+ const now = new Date().toISOString();
5
+ const entry = {
6
+ schemaVersion: 1,
7
+ id: `${now}-${input.type}`,
8
+ type: input.type,
9
+ taskId,
10
+ round: input.round ?? 0,
11
+ message: input.message,
12
+ refs: input.refs ?? [],
13
+ recordedAt: now,
14
+ };
15
+ await appendFile(getLoopPaths(repoRoot, taskId).eventsPath, `${JSON.stringify(entry)}\n`, "utf-8");
16
+ return entry;
17
+ }
18
+ export async function readLoopEvents(repoRoot, taskId) {
19
+ const raw = await readFile(getLoopPaths(repoRoot, taskId).eventsPath, "utf-8").catch(() => "");
20
+ return raw
21
+ .split("\n")
22
+ .map((line) => line.trim())
23
+ .filter(Boolean)
24
+ .map((line) => JSON.parse(line));
25
+ }
@@ -0,0 +1,32 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { listTaskSourceFiles } from "../../task/runtime.js";
5
+ export function sha256Text(text) {
6
+ return createHash("sha256").update(text).digest("hex");
7
+ }
8
+ export async function hashFileIfPresent(filePath) {
9
+ try {
10
+ return sha256Text(await readFile(filePath, "utf-8"));
11
+ }
12
+ catch (error) {
13
+ if (typeof error === "object" &&
14
+ error !== null &&
15
+ "code" in error &&
16
+ error.code === "ENOENT") {
17
+ return "";
18
+ }
19
+ throw error;
20
+ }
21
+ }
22
+ export async function hashTaskSource(repoRoot, taskId) {
23
+ const files = await listTaskSourceFiles(repoRoot, taskId);
24
+ const sourceDir = path.join(repoRoot, ".harness", "tasks", taskId, "source");
25
+ const chunks = [];
26
+ for (const file of files.sort()) {
27
+ const abs = path.isAbsolute(file) ? file : path.join(sourceDir, file);
28
+ const relative = path.relative(sourceDir, abs);
29
+ chunks.push(`${relative}\n${await readFile(abs, "utf-8")}`);
30
+ }
31
+ return sha256Text(chunks.join("\n--- source file ---\n"));
32
+ }
@@ -0,0 +1,8 @@
1
+ export { getLoopPaths } from "./paths.js";
2
+ export { initializeLoop, loadLoopState, refreshLoopState, saveLoopState, } from "./state.js";
3
+ export { appendLoopRound, readLoopRounds } from "./rounds.js";
4
+ export { rewriteLoopContext } from "./context.js";
5
+ export { appendLoopSignal, drainLoopSignals, parseLoopSignalType, pendingLoopSignals, readLoopSignals, } from "./signals.js";
6
+ export { appendLoopEvent, readLoopEvents } from "./events.js";
7
+ export { generateLoopCloseout } from "./closeout.js";
8
+ export { buildLoopPiReviewPrompt, buildLoopCursorFixPrompt, decideNextLoopAutoAction, resolveLoopShellVerifyCommands, runLoopAuto, runLoopCursorFix, runLoopDagAction, runLoopPiReview, runLoopShellVerification, runLoopWorkflowGateSignalAction, runLoopWorkflowAction, validateLoopCursorFixPolicy, } from "./actions.js";
@@ -0,0 +1,17 @@
1
+ import path from "node:path";
2
+ import { getTaskDir } from "../../task/runtime.js";
3
+ export function getLoopPaths(repoRoot, taskId) {
4
+ const loopDir = path.join(getTaskDir(repoRoot, taskId), "loop");
5
+ return {
6
+ loopDir,
7
+ objectivePath: path.join(loopDir, "objective.md"),
8
+ contextPath: path.join(loopDir, "context.md"),
9
+ statePath: path.join(loopDir, "state.json"),
10
+ roundsPath: path.join(loopDir, "rounds.jsonl"),
11
+ signalsPath: path.join(loopDir, "signals.jsonl"),
12
+ closeoutPath: path.join(loopDir, "closeout.md"),
13
+ eventsPath: path.join(loopDir, "events.jsonl"),
14
+ verificationDir: path.join(loopDir, "verification"),
15
+ reviewDir: path.join(loopDir, "reviews"),
16
+ };
17
+ }
@@ -0,0 +1,81 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { getLoopPaths } from "./paths.js";
3
+ import { refreshLoopState } from "./state.js";
4
+ import { appendLoopEvent } from "./events.js";
5
+ function statusForDecision(decision) {
6
+ if (decision === "continue")
7
+ return "running";
8
+ if (decision === "pause")
9
+ return "paused";
10
+ return decision;
11
+ }
12
+ export async function readLoopRounds(repoRoot, taskId) {
13
+ const raw = await readFile(getLoopPaths(repoRoot, taskId).roundsPath, "utf-8");
14
+ return raw
15
+ .split("\n")
16
+ .map((line) => line.trim())
17
+ .filter(Boolean)
18
+ .map((line) => JSON.parse(line));
19
+ }
20
+ export async function appendLoopRound(repoRoot, taskId, input) {
21
+ const rounds = await readLoopRounds(repoRoot, taskId);
22
+ const entry = {
23
+ schemaVersion: 1,
24
+ round: rounds.length + 1,
25
+ recordedAt: new Date().toISOString(),
26
+ action: input.action,
27
+ refs: input.refs ?? [],
28
+ result: input.result,
29
+ verification: input.verification ?? "",
30
+ lesson: input.lesson,
31
+ next: input.next,
32
+ decision: input.decision,
33
+ failureCategory: input.failureCategory ?? "",
34
+ completedCriteria: input.completedCriteria ?? [],
35
+ };
36
+ const paths = getLoopPaths(repoRoot, taskId);
37
+ await appendLoopEvent(repoRoot, taskId, {
38
+ type: "round_start",
39
+ round: entry.round,
40
+ message: `recording round ${entry.round} action ${entry.action}`,
41
+ refs: entry.refs,
42
+ });
43
+ await writeFile(paths.roundsPath, `${rounds.map((round) => JSON.stringify(round)).join("\n")}${rounds.length > 0 ? "\n" : ""}${JSON.stringify(entry)}\n`, "utf-8");
44
+ const previousState = await refreshLoopState(repoRoot, taskId, {
45
+ status: statusForDecision(entry.decision),
46
+ round: entry.round,
47
+ lastDecision: entry.decision,
48
+ lastAction: entry.action,
49
+ lastExecutorRef: entry.refs[0] ?? "",
50
+ openFrontier: entry.next ? [entry.next] : [],
51
+ completedCriteria: entry.completedCriteria,
52
+ });
53
+ if (entry.failureCategory) {
54
+ await refreshLoopState(repoRoot, taskId, {
55
+ failureStreak: {
56
+ category: entry.failureCategory,
57
+ count: previousState.failureStreak.category === entry.failureCategory
58
+ ? previousState.failureStreak.count + 1
59
+ : 1,
60
+ },
61
+ });
62
+ }
63
+ else if (previousState.failureStreak.category) {
64
+ await refreshLoopState(repoRoot, taskId, {
65
+ failureStreak: { category: "", count: 0 },
66
+ });
67
+ }
68
+ await appendLoopEvent(repoRoot, taskId, {
69
+ type: "action_finish",
70
+ round: entry.round,
71
+ message: `${entry.action}: ${entry.result}`,
72
+ refs: entry.refs,
73
+ });
74
+ await appendLoopEvent(repoRoot, taskId, {
75
+ type: "decision",
76
+ round: entry.round,
77
+ message: `decision=${entry.decision}; next=${entry.next}`,
78
+ refs: entry.refs,
79
+ });
80
+ return entry;
81
+ }
@@ -0,0 +1,55 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { getLoopPaths } from "./paths.js";
3
+ import { LOOP_SIGNAL_TYPES, } from "./types.js";
4
+ export function parseLoopSignalType(value) {
5
+ if (LOOP_SIGNAL_TYPES.includes(value)) {
6
+ return value;
7
+ }
8
+ throw new Error(`invalid loop signal type: ${value} (expected ${LOOP_SIGNAL_TYPES.join("|")})`);
9
+ }
10
+ export async function readLoopSignals(repoRoot, taskId) {
11
+ const raw = await readFile(getLoopPaths(repoRoot, taskId).signalsPath, "utf-8").catch(() => "");
12
+ return raw
13
+ .split("\n")
14
+ .map((line) => line.trim())
15
+ .filter(Boolean)
16
+ .map((line) => JSON.parse(line));
17
+ }
18
+ export async function appendLoopSignal(repoRoot, taskId, input) {
19
+ const signals = await readLoopSignals(repoRoot, taskId);
20
+ const now = new Date().toISOString();
21
+ const entry = {
22
+ schemaVersion: 1,
23
+ id: `${now}-${signals.length + 1}`,
24
+ type: input.type,
25
+ message: input.message.trim(),
26
+ urgent: input.urgent ?? false,
27
+ refs: input.refs ?? [],
28
+ createdAt: now,
29
+ drainedAt: "",
30
+ };
31
+ if (!entry.message) {
32
+ throw new Error("loop signal message cannot be empty");
33
+ }
34
+ await writeLoopSignals(repoRoot, taskId, [...signals, entry]);
35
+ return entry;
36
+ }
37
+ export function pendingLoopSignals(signals) {
38
+ return signals.filter((signal) => !signal.drainedAt);
39
+ }
40
+ export async function drainLoopSignals(repoRoot, taskId, ids) {
41
+ if (ids.length === 0)
42
+ return readLoopSignals(repoRoot, taskId);
43
+ const idSet = new Set(ids);
44
+ const now = new Date().toISOString();
45
+ const signals = await readLoopSignals(repoRoot, taskId);
46
+ const drained = signals.map((signal) => idSet.has(signal.id) && !signal.drainedAt
47
+ ? { ...signal, drainedAt: now }
48
+ : signal);
49
+ await writeLoopSignals(repoRoot, taskId, drained);
50
+ return drained;
51
+ }
52
+ async function writeLoopSignals(repoRoot, taskId, signals) {
53
+ await writeFile(getLoopPaths(repoRoot, taskId).signalsPath, signals.map((signal) => JSON.stringify(signal)).join("\n") +
54
+ (signals.length > 0 ? "\n" : ""), "utf-8");
55
+ }
@@ -0,0 +1,116 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { getTaskDir } from "../../task/runtime.js";
3
+ import { repoRelativePath } from "../../shared/path-refs.js";
4
+ import { hashFileIfPresent, hashTaskSource } from "./hash.js";
5
+ import { getLoopPaths } from "./paths.js";
6
+ import { defaultContext, defaultObjective } from "./templates.js";
7
+ import { appendLoopEvent } from "./events.js";
8
+ async function exists(filePath) {
9
+ try {
10
+ await access(filePath);
11
+ return true;
12
+ }
13
+ catch (error) {
14
+ if (typeof error === "object" &&
15
+ error !== null &&
16
+ "code" in error &&
17
+ error.code === "ENOENT") {
18
+ return false;
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+ function rel(repoRoot, filePath) {
24
+ return repoRelativePath(repoRoot, filePath);
25
+ }
26
+ export async function buildLoopState(repoRoot, taskId, paths = getLoopPaths(repoRoot, taskId), previous) {
27
+ const now = new Date().toISOString();
28
+ const objectiveHash = await hashFileIfPresent(paths.objectivePath);
29
+ const contextHash = await hashFileIfPresent(paths.contextPath);
30
+ const sourceHash = await hashTaskSource(repoRoot, taskId);
31
+ const previousSourceHash = previous?.sourceHash ?? sourceHash;
32
+ return {
33
+ schemaVersion: 1,
34
+ taskId,
35
+ status: previous?.status ?? "draft",
36
+ round: previous?.round ?? 0,
37
+ maxRounds: previous?.maxRounds ?? 8,
38
+ objectivePath: rel(repoRoot, paths.objectivePath),
39
+ contextPath: rel(repoRoot, paths.contextPath),
40
+ roundsPath: rel(repoRoot, paths.roundsPath),
41
+ objectiveHash,
42
+ contextHash,
43
+ sourceHash,
44
+ freshness: {
45
+ objectiveHash,
46
+ contextHash,
47
+ sourceHash,
48
+ sourceStale: sourceHash !== previousSourceHash,
49
+ lastCheckedAt: now,
50
+ },
51
+ lastDecision: previous?.lastDecision ?? "init",
52
+ lastAction: previous?.lastAction ?? "init",
53
+ lastExecutorRef: previous?.lastExecutorRef ?? "",
54
+ openFrontier: previous?.openFrontier ?? ["choose the next bounded action"],
55
+ completedCriteria: previous?.completedCriteria ?? [],
56
+ failureStreak: previous?.failureStreak ?? { category: "", count: 0 },
57
+ createdAt: previous?.createdAt ?? now,
58
+ updatedAt: now,
59
+ };
60
+ }
61
+ export async function saveLoopState(repoRoot, taskId, state) {
62
+ const paths = getLoopPaths(repoRoot, taskId);
63
+ await writeFile(paths.statePath, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
64
+ }
65
+ export async function loadLoopState(repoRoot, taskId) {
66
+ const paths = getLoopPaths(repoRoot, taskId);
67
+ const raw = await readFile(paths.statePath, "utf-8");
68
+ return JSON.parse(raw);
69
+ }
70
+ export async function refreshLoopState(repoRoot, taskId, patch = {}) {
71
+ const previous = await loadLoopState(repoRoot, taskId);
72
+ const state = await buildLoopState(repoRoot, taskId, undefined, {
73
+ ...previous,
74
+ ...patch,
75
+ });
76
+ await saveLoopState(repoRoot, taskId, state);
77
+ return state;
78
+ }
79
+ export async function initializeLoop(repoRoot, taskId, options = {}) {
80
+ const taskDir = getTaskDir(repoRoot, taskId);
81
+ if (!(await exists(taskDir))) {
82
+ throw new Error(`task does not exist: ${taskId}`);
83
+ }
84
+ const paths = getLoopPaths(repoRoot, taskId);
85
+ const stateExists = await exists(paths.statePath);
86
+ if (stateExists && !options.recover) {
87
+ throw new Error(`loop already initialized for task: ${taskId}`);
88
+ }
89
+ await mkdir(paths.loopDir, { recursive: true });
90
+ if (!(await exists(paths.objectivePath))) {
91
+ await writeFile(paths.objectivePath, defaultObjective(taskId), "utf-8");
92
+ }
93
+ if (!(await exists(paths.contextPath))) {
94
+ await writeFile(paths.contextPath, defaultContext(), "utf-8");
95
+ }
96
+ if (!(await exists(paths.roundsPath))) {
97
+ await writeFile(paths.roundsPath, "", "utf-8");
98
+ }
99
+ if (!(await exists(paths.signalsPath))) {
100
+ await writeFile(paths.signalsPath, "", "utf-8");
101
+ }
102
+ if (!(await exists(paths.eventsPath))) {
103
+ await writeFile(paths.eventsPath, "", "utf-8");
104
+ }
105
+ const previous = stateExists ? await loadLoopState(repoRoot, taskId) : undefined;
106
+ const state = await buildLoopState(repoRoot, taskId, paths, {
107
+ ...previous,
108
+ maxRounds: options.maxRounds ?? previous?.maxRounds ?? 8,
109
+ });
110
+ await saveLoopState(repoRoot, taskId, state);
111
+ await appendLoopEvent(repoRoot, taskId, {
112
+ type: "loop_start",
113
+ message: stateExists ? "loop recovered" : "loop initialized",
114
+ });
115
+ return { created: !stateExists, paths, state };
116
+ }
@@ -0,0 +1,54 @@
1
+ export function defaultObjective(taskId) {
2
+ return [
3
+ "# Objective",
4
+ "",
5
+ "## Goal",
6
+ `- Complete the long-running task ${taskId}.`,
7
+ "",
8
+ "## Success Criteria",
9
+ "- Loop success criteria are copied or summarized from source/需求.md.",
10
+ "- Completion requires current shell verification evidence.",
11
+ "",
12
+ "## Scope",
13
+ "- In: work described by the task source materials.",
14
+ "- Out: unrelated repository changes.",
15
+ "",
16
+ "## Constraints",
17
+ "- Allowed paths: see source/执行约束.md.",
18
+ "- Forbidden paths: see source/执行约束.md.",
19
+ "- Verification: see source/执行约束.md.",
20
+ "",
21
+ "## Completion Policy",
22
+ "- Required evidence: shell verification output and referenced executor facts.",
23
+ "- Required review: as specified by the task source or DAG gates.",
24
+ "- Stop conditions: complete, paused, blocked, or max rounds reached.",
25
+ "",
26
+ ].join("\n");
27
+ }
28
+ export function defaultContext() {
29
+ return [
30
+ "# Loop Context",
31
+ "",
32
+ "## Current State",
33
+ "- Completed: none recorded yet.",
34
+ "- Open frontier: initialize loop and choose the next bounded action.",
35
+ "- Current hypothesis: storage state must be established before executor integration.",
36
+ "",
37
+ "## Verified Facts",
38
+ "- No loop rounds have been recorded yet.",
39
+ "",
40
+ "## Last Round Lessons",
41
+ "- What worked: none yet.",
42
+ "- What failed: none yet.",
43
+ "- Avoid next round: do not treat chat history as durable state.",
44
+ "",
45
+ "## Next Suggested Action",
46
+ "- Action: record a bounded round with canonical evidence refs.",
47
+ "- Why: loop context should be derived from durable facts.",
48
+ "- Expected evidence: rounds.jsonl entry and rewritten context.md.",
49
+ "",
50
+ "## Risk Notes",
51
+ "- Completion still requires deterministic verification.",
52
+ "",
53
+ ].join("\n");
54
+ }
@@ -0,0 +1,28 @@
1
+ export const LOOP_STATUSES = [
2
+ "draft",
3
+ "running",
4
+ "paused",
5
+ "blocked",
6
+ "complete",
7
+ ];
8
+ export const LOOP_DECISIONS = [
9
+ "continue",
10
+ "pause",
11
+ "blocked",
12
+ "complete",
13
+ ];
14
+ export const LOOP_EVENT_TYPES = [
15
+ "loop_start",
16
+ "round_start",
17
+ "action_start",
18
+ "action_finish",
19
+ "context_rewrite",
20
+ "decision",
21
+ "loop_finish",
22
+ ];
23
+ export const LOOP_SIGNAL_TYPES = [
24
+ "human_followup",
25
+ "approval",
26
+ "scope_changed",
27
+ "review_feedback",
28
+ ];
package/docs/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Documentation Index
2
+
3
+ `docs/` is the governance root for loop-agent. It contains workflow rules, methodology, verification rules, execution plans, reports, progress logs, decisions, and reusable templates.
4
+
5
+ Top-level `AGENTS.md` is the operating map. Durable knowledge belongs here: decisions, contracts, plans, verification evidence, debugging notes, and reusable process rules should be recorded under `docs/` instead of staying only in chat.
6
+
7
+ ## Core Documents
8
+
9
+ - `development-principles.md` — repository development principles
10
+ - `feature-workflow.md` — bounded feature workflow
11
+ - `verification-matrix.md` — verification command selection
12
+ - `loop-agent-harness.md` — runtime and command surface overview
13
+ - `agent-dag-runner.md` — Agent DAG runner guide
14
+ - `cursor-executor-usage.md` — Cursor executor usage
15
+ - `dynamic-workflow-dag-engine-roadmap.md` — Dynamic Workflow DAG Engine roadmap and fit analysis
16
+
17
+ ## Methodology
18
+
19
+ - `harness-methodology-tdd.md` — TDD discipline for behavior changes and bug fixes
20
+ - `harness-methodology-verification.md` — verification discipline before completion claims
21
+ - `harness-methodology-debugging.md` — systematic debugging workflow before fixes
22
+
23
+ ## Artifacts
24
+
25
+ - `design/README.md` — draft design notes and implementation contracts
26
+ - `exec-plans/active/README.md` — active execution plans
27
+ - `exec-plans/completed/README.md` — completed execution plans
28
+ - `progress/README.md` — progress handoff logs
29
+ - `reports/README.md` — verification and audit reports
30
+ - `decisions/README.md` — architecture decisions
31
+ - `templates/` — reusable planning, reporting, and DAG templates
32
+
33
+ ## Repository Skills
34
+
35
+ - `../skills/loop-agent/` — loop-agent's own skill instructions and references.
36
+ - Each additional skill uses its own subdirectory under repository-root `../skills/`; these local copies are referenced by DAG templates so maintenance does not depend on external agent skill directories.
37
+
38
+ ## Templates
39
+
40
+ - `templates/project-start-checklist.md` — pre-work checklist
41
+ - `templates/feature-spec.md` — bounded feature specification
42
+ - `templates/sprint-contract.md` — implementation contract and acceptance criteria
43
+ - `templates/exec-plan.md` — execution plan for non-trivial work
44
+ - `templates/progress-log.md` — progress and handoff log
45
+ - `templates/qa-report.md` — verification and QA evidence
46
+ - `templates/adr.md` — architecture decision record
47
+
48
+ ## Maintenance
49
+
50
+ After docs changes, run:
51
+
52
+ ```bash
53
+ bash scripts/check-repo.sh
54
+ ```
55
+
56
+ On Windows, run Bash scripts through Git Bash or a configured compatible Bash. Use platform-native paths for actual file operations; reserve `/` for repo refs, JSON/Markdown evidence refs, and glob conventions.
57
+
58
+ For a full local gate, run:
59
+
60
+ ```bash
61
+ bash scripts/ci.sh
62
+ ```
@@ -0,0 +1,158 @@
1
+ # Agent DAG Recovery Playbook
2
+
3
+ > **关联**:[`agent-dag-runner.md`](agent-dag-runner.md)(CLI 与 run 语义)· [`templates/agent-dag-decision-gate.prompt.md`](templates/agent-dag-decision-gate.prompt.md)(Decision Gate 消费 recovery 证据)
4
+
5
+ ## 定位
6
+
7
+ Agent DAG **recovery planning 是只读、派生、advisory** 的。`dag report` 与 `buildDagDecisionGateEvidence()` 从 `.harness/dag-runs/` 的 canonical facts 聚合 `normalizedFailureCategory` → `recoveryRecommendation`,供人工或 Decision Gate prompt 消费。
8
+
9
+ **非目标(本 playbook 不覆盖、runner 不实现):**
10
+
11
+ - 自动 retry / resume 节点执行
12
+ - 修改 `completed/` 或 `paused/` 下的历史 run facts
13
+ - 把 `autoRetryEligible` 当作 runtime 触发器
14
+ - 仅凭 recovery 派生字段自动 approve Decision Gate
15
+
16
+ ## 快速命令
17
+
18
+ ```bash
19
+ cd .
20
+
21
+ # 全局 runtime 健康(active/paused/completed 摘要 + healthIssues;advisoryOnly)
22
+ npm run dev -- dag doctor
23
+
24
+ # 单 run 生命周期(approvalFlow、hasHumanApproval、nextRecommendedAction)
25
+ npm run dev -- dag status --run-id <run-id>
26
+
27
+ # 聚焦最新 paused run(--paused-latest ≡ --lifecycle paused --latest)
28
+ npm run dev -- dag report --paused-latest [--json|--markdown]
29
+
30
+ # 默认 compact Markdown 表格
31
+ npm run dev -- dag report --run-id <run-id>
32
+
33
+ # 机器可读 JSON(含 primaryFailure / primaryRecovery / downstreamSkippedNodes)
34
+ npm run dev -- dag report --run-id <run-id> --json
35
+
36
+ # 人类交接 Recovery Plan(四段结构化 Markdown)
37
+ npm run dev -- dag report --run-id <run-id> --markdown
38
+
39
+ # 过滤器
40
+ npm run dev -- dag report --failed-only # 仅失败/需恢复
41
+ npm run dev -- dag report --latest --failed-only # 最新一条需恢复 run
42
+ npm run dev -- dag report --action retry-node # 按 primaryRecovery.action 筛选
43
+ npm run dev -- dag report --lifecycle paused --action resume-or-reject
44
+
45
+ # Decision Gate envelope dry-run(不 resume/retry;validate 无效时 exit 1)
46
+ npm run dev -- dag decision inspect --run-id <run-id> [--node-id <node-id>]
47
+ npm run dev -- dag decision validate --run-id <run-id> [--node-id <node-id>]
48
+ ```
49
+
50
+ ### Paused run 操作员路径
51
+
52
+ 1. `dag report --paused-latest --json` 或 `dag doctor` — 定位最新 paused run 与 `primaryRecovery`
53
+ 2. `dag status --run-id <id>` — 读 `approvalFlow`、`escalationArtifactPath`、`pendingNodes`
54
+ 3. (可选)`dag decision validate --run-id <id>` — envelope preflight
55
+ 4. `dag approve --run-id <id> --option <option-id>` → `dag resume --run-id <id>`;或 `dag reject --run-id <id> --reason "..."`
56
+
57
+ 精确 approval 顺序见 [`agent-dag-runner.md`](agent-dag-runner.md) §Paused lifecycle。
58
+
59
+ Decision Gate prompt 侧:`buildDagDecisionGateEvidence()`(`./src/core/dag-decision-evidence.ts`)从 `DagRunReportEntry` 生成 prompt-friendly 摘要,字段与 JSON report 对齐,**不**写回 run state。
60
+
61
+ ## `dag report --json` schema 锁定
62
+
63
+ - **Schema 文件**:`docs/templates/agent-dag-report.schema.json`
64
+ - **Envelope**:`{ schemaVersion: 1, runs: DagRunReportEntry[] }`
65
+ - **稳定消费字段**(Decision Gate / tooling 应依赖):`primaryFailure`、`primaryRecovery`、`downstreamSkippedNodes`、`recoveryRecommendation`、`normalizedFailureCategory`;node 级 `decisionEnvelope`、`artifacts`;paused 级 `pausedByNodeId`、`pauseReason`
66
+ - **测试**:`./test/dag-report.test.ts` §`dag report JSON schema contract` 对 fixture run 做 schema 校验
67
+ - **变更策略**:breaking 字段变更须 bump `schemaVersion` 并同步 schema 文件与测试
68
+
69
+ ## Recovery Action 枚举
70
+
71
+ | Action | 含义 | 典型触发 |
72
+ |--------|------|----------|
73
+ | `none` | 无需恢复 | 成功完成 |
74
+ | `monitor` | 进行中,等待结束 | `PENDING` / `RUNNING` |
75
+ | `retry-node` | 修复瞬态条件后可重跑节点 | timeout;executor 瞬态(network/quota/rate-limit/unavailable) |
76
+ | `rerun-after-fix` | 先修根因再重跑 | auth、validation、shell-command、static-error、非瞬态 executor |
77
+ | `resume-or-reject` | 人工审批后继续或拒绝 | paused + decision-envelope / human-required |
78
+ | `manual-review` | 人工审查后再定路径 | write-guard、human-rejected、unknown、非 paused 的 decision-envelope |
79
+ | `inspect-upstream` | 先查上游失败 | SKIPPED 下游节点 |
80
+ | `unknown` | 未映射类别(不应出现在正常派生路径) | 内部兜底 |
81
+
82
+ ## 类别 → 动作 → 操作员指引
83
+
84
+ | Normalized category | Recovery action | Operator guidance | Anti-patterns |
85
+ |---------------------|-----------------|-------------------|---------------|
86
+ | `success` | `none` | 归档验收;按需 review artifacts | 对成功 run 发起 retry |
87
+ | `timeout` | `retry-node` | 查日志/artifacts 确认瞬态;人工重跑节点 | 未查根因就循环重试;指望 runner 自动 retry |
88
+ | `executor`(network/quota/rate-limit/unavailable) | `retry-node` | 等后端/配额恢复后重跑 | 把 auth/validation 误判为瞬态 executor |
89
+ | `executor`(其他 raw) | `rerun-after-fix` | 查 executor.jsonl、node result | 盲目 retry 非瞬态 backend 错误 |
90
+ | `auth` | `rerun-after-fix` | 更新 API key/凭证后重跑 | 在凭证未修复时 retry |
91
+ | `write-guard` | `manual-review` | 审 writeSet/writePolicy、prompt、result.summary | read-only 节点写根 `artifacts/`;扩大 writeSet 掩盖违规 |
92
+ | `validation` | `rerun-after-fix` | 修 schema/output/test 后再跑 | 跳过验证直接 approve |
93
+ | `shell-command` | `rerun-after-fix` | 读 stdout/stderr、修命令或 repo 状态 | 只重跑 shell 不改命令 |
94
+ | `static-error` | `rerun-after-fix` | 查 static config 与 emitted markdown | 当 LLM 节点 retry |
95
+ | `decision-envelope`(paused) | `resume-or-reject` | `dag approve --run-id <id> --option <option-id>` / `dag reject --run-id <id> --reason "..."` → `dag resume --run-id <id>` | 未读 envelope 就 approve;用 recovery 字段单独 auto-approve |
96
+ | `decision-envelope`(非 paused) | `manual-review` | 读 decision.envelope.json / validation artifact | 绕过 Decision Gate schema |
97
+ | `human-required`(paused) | `resume-or-reject` | 提供人工输入 → approve/resume | 在 escalation 未解决时 resume |
98
+ | `human-required`(非 paused) | `manual-review` | 读 human-escalation artifacts | 忽略 `requiresHuman` |
99
+ | `human-rejected` | `manual-review` | 修订 contract/source;**新 run** | 对同一 contract 自动 retry |
100
+ | `skipped` | `inspect-upstream` | 修上游 ERROR/SKIPPED 再考虑下游 | 直接 retry SKIPPED 节点 |
101
+ | `unknown` | `manual-review` | 读 state.json、executor.jsonl、node artifacts | 假设 `autoRetryEligible` 会触发执行 |
102
+
103
+ ## Handoff Recovery Plan 结构
104
+
105
+ `dag report --markdown` 的 **Recovery Plan** 含四段(与 JSON 稳定字段一一对应):
106
+
107
+ 1. **Primary Failure** — `primaryFailure`(node 或 run scope)
108
+ 2. **Recovery Action** — `primaryRecovery`(action、summary、reason、flags、commandHint)
109
+ 3. **Blocked Downstream / Skipped Nodes** — `downstreamSkippedNodes`
110
+ 4. **Recommended Operator Action** — 面向操作员的步骤摘要
111
+
112
+ 保存 handoff 时重定向到平台临时目录或 `docs/reports/`,不要写入 `.harness/dag-runs/`。
113
+
114
+ ## Decision Gate 消费约定
115
+
116
+ 1. 优先 `dag report --json` 或 `buildDagDecisionGateEvidence()` 的 **verified** 派生摘要。
117
+ 2. 映射到 `decision` / `nextAction` 须保守;recovery 证据是 **advisory only, not an execution directive**。
118
+ 3. `autoRetryEligible: true` 仅表示「规划上可人工重试」,**不**触发 runner。
119
+ 4. paused run 的人类路径仍是 M5 CLI:`dag approve --run-id <id> --option <option-id>` / `dag reject --run-id <id> --reason "..."` / `dag resume --run-id <id>`(见 [`agent-dag-runner.md`](agent-dag-runner.md) §Decision Gate)。
120
+ 5. Envelope 干跑:`dag decision inspect|validate` 重解析 run facts;`validate` 无效时 exit 1;**不**写 artifact、**不** resume。
121
+
122
+ ## Active stale run recovery(advisory detection)
123
+
124
+ `dag doctor` 与 `dag status` 通过 `detectDagRunHealthIssues()` 检测 lifecycle 不一致,**不** mutate run facts。
125
+
126
+ | Code | 典型场景 | 操作员指引 |
127
+ |------|----------|------------|
128
+ | `terminal-in-active` | run 已完成但 `active/<run-id>/` 残留 | 对照 `completed/` canonical facts;手动 archive 或删除 stale 目录 |
129
+ | `paused-in-active` | pause 后目录未迁至 `paused/` | `dag doctor` 诊断;修复 facts 后再 approve/resume |
130
+ | `lifecycle-status-mismatch` | `paused/` 下 status 非 paused | 同上 |
131
+ | `missing-approval-artifact` | approve 后 artifact 缺失 | 勿 resume;re-approve 或 restore artifact |
132
+ | `non-terminal-in-completed` | completed 目录 status 异常 | manual-review only |
133
+ | `run-id-mismatch` / `missing-state-json` | 目录损坏或命名错误 | Inspect;勿 auto-mutate completed facts |
134
+
135
+ **Deferred runtime**:无 `dag recover apply` 或自动 cleanup;未来可能增加只读 `dag recover plan`(设计占位,未实现)。
136
+
137
+ ## 事实源与边界
138
+
139
+ | 类型 | 位置 | 规则 |
140
+ |------|------|------|
141
+ | Canonical run facts | `.harness/dag-runs/{active\|paused\|completed}/<run-id>/` | **只读**;report 不写回 |
142
+ | 派生 report | stdout / 重定向文件 | 可随时再生 |
143
+ | 工作块摘要 | 根 `artifacts/` | 非 per-run 历史;read-only DAG 节点不得写 |
144
+
145
+ ## 验证
146
+
147
+ ```bash
148
+ cd . && npx vitest run \
149
+ test/dag-report.test.ts \
150
+ test/dag-recovery-recommendation.test.ts \
151
+ test/dag-decision-gate-recovery-dogfood.test.ts \
152
+ test/dag-decision-evidence.test.ts \
153
+ test/dag-decision-envelope.test.ts \
154
+ test/dag-approve-resume.test.ts \
155
+ test/cli-contract.test.ts
156
+ ```
157
+
158
+ 实现细节与映射逻辑:`./src/core/dag-recovery-recommendation.ts`、`dag-report.ts`、`dag-decision-evidence.ts`。