@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,88 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const INLINE_PROMPT_FIELD = "subtask_prompt";
5
+ const MARKDOWN_PROMPT_FIELD = "subtask_prompt_markdown";
6
+ const HYPHENATED_MARKDOWN_PROMPT_FIELD = "subtask_prompt-markdown";
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function hasOwn(record, key) {
11
+ return Object.hasOwn(record, key);
12
+ }
13
+ function taskLabel(task, index) {
14
+ return typeof task.id === "string" && task.id.trim() ? task.id : `#${index}`;
15
+ }
16
+ export function assertDagPromptSourceRule(raw, options) {
17
+ if (!isRecord(raw))
18
+ return;
19
+ if (!Array.isArray(raw.tasks))
20
+ return;
21
+ raw.tasks.forEach((taskRaw, index) => {
22
+ if (!isRecord(taskRaw))
23
+ return;
24
+ const label = taskLabel(taskRaw, index);
25
+ if (hasOwn(taskRaw, HYPHENATED_MARKDOWN_PROMPT_FIELD)) {
26
+ throw new Error(`task ${label} must use ${MARKDOWN_PROMPT_FIELD}, not ${HYPHENATED_MARKDOWN_PROMPT_FIELD}`);
27
+ }
28
+ const hasInline = hasOwn(taskRaw, INLINE_PROMPT_FIELD);
29
+ const hasMarkdown = hasOwn(taskRaw, MARKDOWN_PROMPT_FIELD);
30
+ if (hasInline === hasMarkdown) {
31
+ throw new Error(`task ${label} must contain exactly one of ${INLINE_PROMPT_FIELD} or ${MARKDOWN_PROMPT_FIELD}`);
32
+ }
33
+ if (hasInline) {
34
+ const prompt = taskRaw[INLINE_PROMPT_FIELD];
35
+ if (typeof prompt !== "string" || prompt.trim().length === 0) {
36
+ throw new Error(`task ${label} ${INLINE_PROMPT_FIELD} must be a non-empty string`);
37
+ }
38
+ }
39
+ if (hasMarkdown) {
40
+ if (!options.allowMarkdown) {
41
+ throw new Error(`task ${label} uses ${MARKDOWN_PROMPT_FIELD}; load DAGs with loadDagSpecFromFile so markdown prompts can be resolved`);
42
+ }
43
+ const markdownPath = taskRaw[MARKDOWN_PROMPT_FIELD];
44
+ if (typeof markdownPath !== "string" ||
45
+ markdownPath.trim().length === 0) {
46
+ throw new Error(`task ${label} ${MARKDOWN_PROMPT_FIELD} must be a non-empty string`);
47
+ }
48
+ }
49
+ });
50
+ }
51
+ export async function normalizeDagPromptSources(raw, dagDir) {
52
+ assertDagPromptSourceRule(raw, { allowMarkdown: true });
53
+ if (!isRecord(raw) || !Array.isArray(raw.tasks))
54
+ return raw;
55
+ const tasks = await Promise.all(raw.tasks.map(async (taskRaw, index) => {
56
+ if (!isRecord(taskRaw) || !hasOwn(taskRaw, MARKDOWN_PROMPT_FIELD)) {
57
+ return taskRaw;
58
+ }
59
+ const label = taskLabel(taskRaw, index);
60
+ const markdownPath = String(taskRaw[MARKDOWN_PROMPT_FIELD]);
61
+ const resolvedPath = path.resolve(dagDir, markdownPath);
62
+ let content;
63
+ try {
64
+ content = await readFile(resolvedPath, "utf-8");
65
+ }
66
+ catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ throw new Error(`failed to read ${MARKDOWN_PROMPT_FIELD} for task ${label}: ${resolvedPath}: ${message}`);
69
+ }
70
+ if (content.trim().length === 0) {
71
+ throw new Error(`task ${label} ${MARKDOWN_PROMPT_FIELD} resolved to an empty file: ${resolvedPath}`);
72
+ }
73
+ const { [MARKDOWN_PROMPT_FIELD]: _markdown, ...rest } = taskRaw;
74
+ return {
75
+ ...rest,
76
+ [INLINE_PROMPT_FIELD]: content,
77
+ subtask_prompt_source: {
78
+ type: "markdown",
79
+ path: markdownPath,
80
+ sha256: createHash("sha256").update(content).digest("hex"),
81
+ },
82
+ };
83
+ }));
84
+ return {
85
+ ...raw,
86
+ tasks,
87
+ };
88
+ }
@@ -0,0 +1,130 @@
1
+ import { formatStdoutPreview, formatUpstreamArtifactPointerMap, } from "./upstream-artifacts.js";
2
+ export const MAX_UPSTREAM_CHARS = 2_000;
3
+ /** Shared bullets for DAG authoring templates, docs, and planner-node envelopes. */
4
+ export const DAG_AUTHORING_GUIDANCE = [
5
+ "Prefer same-rank parallel read-only scouts over unnecessary serial depends_on chains.",
6
+ "Add depends_on only when a child truly needs upstream output; default to independent ranks.",
7
+ "Every task must explicitly declare executor; defaults.executor is schema-only, not a runtime fallback.",
8
+ "Cursor remains a first-class executor for scouts and exclusive implementers when it fits the task.",
9
+ "exclusive nodes require narrow, concrete, disjoint writeSet paths; never use ** or repo root.",
10
+ "Read-only nodes must not write repository files, including root artifacts/**; return findings in node output only.",
11
+ "If the DAG is a single linear chain, challenge whether read-only work can run in parallel ranks.",
12
+ ];
13
+ const PLANNER_AUTHORING_ROLES = new Set(["planner"]);
14
+ function formatBulletList(items, emptyLabel) {
15
+ if (!items || items.length === 0)
16
+ return emptyLabel;
17
+ return items.map((item) => `- ${item}`).join("\n");
18
+ }
19
+ function formatPaths(paths) {
20
+ if (!paths || paths.length === 0)
21
+ return "(none)";
22
+ return paths.join(", ");
23
+ }
24
+ function resolveWritePolicy(task, spec) {
25
+ return task.writePolicy ?? spec.defaults?.writePolicy ?? "(not specified)";
26
+ }
27
+ function resolveOutputContract(task) {
28
+ return task.outputContract ?? "(none)";
29
+ }
30
+ function formatAuthoringGuidanceBlock() {
31
+ return DAG_AUTHORING_GUIDANCE.map((item) => `- ${item}`).join("\n");
32
+ }
33
+ function formatExecutor(task) {
34
+ return (task.executor ??
35
+ "(not specified — declare executor explicitly on every task)");
36
+ }
37
+ function formatResolvedSkillInstructions(instructions) {
38
+ if (!instructions || instructions.length === 0)
39
+ return "(none)";
40
+ return instructions
41
+ .map((instruction) => instruction.promptText)
42
+ .join("\n\n---\n\n");
43
+ }
44
+ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHARS) {
45
+ const sections = [];
46
+ for (const depId of task.depends_on) {
47
+ const record = upstream[depId];
48
+ const stdout = record?.stdout?.trim() ? record.stdout : "";
49
+ const assistantText = record?.assistantText?.trim()
50
+ ? record.assistantText
51
+ : "";
52
+ const upstreamText = stdout || assistantText;
53
+ if (!record || record.status !== "FINISHED" || !upstreamText)
54
+ continue;
55
+ const artifactKind = stdout ? "stdout" : "assistant";
56
+ const artifactPath = stdout
57
+ ? record.stdoutArtifactPath
58
+ : record.assistantArtifactPath;
59
+ const { preview, truncated } = formatStdoutPreview(upstreamText, maxChars);
60
+ let section = `## Upstream output: ${depId}\n${preview}`;
61
+ if (truncated && artifactPath) {
62
+ section += formatUpstreamArtifactPointerMap(record, artifactKind);
63
+ }
64
+ sections.push(section);
65
+ }
66
+ return sections.join("\n\n");
67
+ }
68
+ function buildReadOnlyBoundary(writePolicy) {
69
+ if (writePolicy !== "read-only")
70
+ return [];
71
+ return [
72
+ "Read-only boundary: do not write, create, edit, delete, stage, or commit any repository files.",
73
+ "This includes root artifacts/**; root artifacts/ is not a read-only scratchpad.",
74
+ "Return findings in the assistant response/stdout only; the DAG runner persists node artifacts under .harness/dag-runs/<state>/<run-id>/<node-id>/.",
75
+ ];
76
+ }
77
+ export function buildDagNodePromptEnvelope(input) {
78
+ const { spec, task, upstream, resolvedSkills = [], resolvedSkillInstructions = [], maxUpstreamChars = MAX_UPSTREAM_CHARS, } = input;
79
+ const objective = spec.objective ?? spec.title;
80
+ const successCriteria = formatBulletList(spec.successCriteria, "(none specified)");
81
+ const globalConstraints = formatBulletList(spec.globalConstraints, "(none specified)");
82
+ const role = task.role ?? "(not specified)";
83
+ const writePolicy = resolveWritePolicy(task, spec);
84
+ const skillsSection = resolvedSkills.length > 0
85
+ ? resolvedSkills.map((skill) => `- ${skill}`).join("\n")
86
+ : "(none)";
87
+ const skillInstructionsSection = formatResolvedSkillInstructions(resolvedSkillInstructions);
88
+ const upstreamContext = buildUpstreamContext(task, upstream, maxUpstreamChars);
89
+ const readOnlyBoundary = buildReadOnlyBoundary(writePolicy);
90
+ const sections = [
91
+ `<dag_objective>\n${objective}\n</dag_objective>`,
92
+ `<success_criteria>\n${successCriteria}\n</success_criteria>`,
93
+ `<global_constraints>\n${globalConstraints}\n</global_constraints>`,
94
+ [
95
+ "<node_contract>",
96
+ `Role: ${role}`,
97
+ `Executor: ${formatExecutor(task)}`,
98
+ `Write policy: ${writePolicy}`,
99
+ `Allowed paths: ${formatPaths(task.allowedPaths)}`,
100
+ `Forbidden paths: ${formatPaths(task.forbiddenPaths)}`,
101
+ `Write set: ${formatPaths(task.writeSet)}`,
102
+ `Output contract: ${resolveOutputContract(task)}`,
103
+ ...readOnlyBoundary,
104
+ "</node_contract>",
105
+ ].join("\n"),
106
+ [
107
+ "<required_skills>",
108
+ "Before acting, apply the following skills:",
109
+ skillsSection,
110
+ "</required_skills>",
111
+ ].join("\n"),
112
+ [
113
+ "<resolved_skill_instructions>",
114
+ "The DAG runner resolved these skill files deterministically. Apply the bounded instructions below when present. Missing/error entries mean only the skill name is available; do not assume hidden Pi ResourceLoader skills.",
115
+ skillInstructionsSection,
116
+ "</resolved_skill_instructions>",
117
+ ].join("\n"),
118
+ ];
119
+ if (task.role && PLANNER_AUTHORING_ROLES.has(task.role)) {
120
+ sections.push(`<authoring_guidance>\n${formatAuthoringGuidanceBlock()}\n</authoring_guidance>`);
121
+ }
122
+ if (upstreamContext) {
123
+ sections.push(`<upstream_context>\n${upstreamContext}\n</upstream_context>`);
124
+ }
125
+ else {
126
+ sections.push("<upstream_context>\n(none)\n</upstream_context>");
127
+ }
128
+ sections.push(`<task>\n${task.subtask_prompt}\n</task>`);
129
+ return sections.join("\n\n");
130
+ }
@@ -0,0 +1,404 @@
1
+ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getArtifactPath, validateArtifactContent } from '../../shared/artifacts-core.js';
4
+ import { parseCursorRunLog, resolveRunStatus } from '../../records/one-shot-runs.js';
5
+ import { listAllDagRunEntries, readDagRunSpec, readDagRunState, } from './lifecycle.js';
6
+ import { matchGlob } from '../../shared/reference-context.js';
7
+ import { getTaskPaths } from '../../task/runtime.js';
8
+ const TASKS_DIR = path.join('.harness', 'tasks');
9
+ const REQUIRED_ARTIFACTS = [
10
+ { fileName: '修改记录.md', step: 'implement', label: 'modify log' },
11
+ { fileName: '验证结果.md', step: null, label: 'verify result' },
12
+ ];
13
+ const TEMPLATE_MARKERS = [/TODO/i, /REPLACE/i, /<template/i, /待替换/];
14
+ const VERIFY_MARKERS = [/npm\s+(run\s+)?test/i, /vitest/i, /check-repo\.sh/i, /typecheck/i, /lint/i, /CI/i, /verify/i, /验证/];
15
+ const CLOSEOUT_NAMES = ['复盘报告.md', 'closeout.md', 'closeout.json', 'handoff.md'];
16
+ export async function runTaskReconcile(repoRoot, options) {
17
+ const glob = options.glob || '*';
18
+ const patchMode = Boolean(options.patch);
19
+ const taskIds = await listMatchingTaskIds(repoRoot, glob);
20
+ const dagRuns = await loadDagRunFacts(repoRoot);
21
+ const oneShotRuns = await loadOneShotRunFacts(repoRoot);
22
+ const misplacedArtifacts = await listMisplacedCodeAgentArtifacts(repoRoot);
23
+ const patches = [];
24
+ const tasks = [];
25
+ for (const taskId of taskIds) {
26
+ const report = await reconcileOneTask(repoRoot, taskId, dagRuns, oneShotRuns, misplacedArtifacts, patchMode, patches);
27
+ tasks.push(report);
28
+ }
29
+ const findings = tasks.flatMap((task) => task.findings);
30
+ const errors = findings.filter((finding) => finding.severity === 'error').length;
31
+ const warnings = findings.filter((finding) => finding.severity === 'warning').length;
32
+ const infos = findings.filter((finding) => finding.severity === 'info').length;
33
+ return {
34
+ schemaVersion: 1,
35
+ generatedAt: new Date().toISOString(),
36
+ glob,
37
+ mode: patchMode ? 'patch' : 'report-only',
38
+ ok: findings.length === 0,
39
+ summary: {
40
+ tasksScanned: tasks.length,
41
+ findings: findings.length,
42
+ errors,
43
+ warnings,
44
+ infos,
45
+ patches: patches.length,
46
+ },
47
+ tasks,
48
+ patches,
49
+ };
50
+ }
51
+ export function formatTaskReconcileJson(report) {
52
+ return `${JSON.stringify(report, null, 2)}\n`;
53
+ }
54
+ export function formatTaskReconcileMarkdown(report) {
55
+ const lines = [
56
+ '# Task Reconcile Report',
57
+ '',
58
+ `- glob: \`${report.glob}\``,
59
+ `- mode: \`${report.mode}\``,
60
+ `- tasks scanned: ${report.summary.tasksScanned}`,
61
+ `- findings: ${report.summary.findings} (${report.summary.errors} errors, ${report.summary.warnings} warnings, ${report.summary.infos} info)`,
62
+ `- patches: ${report.summary.patches}`,
63
+ '',
64
+ '| Task | Severity | Kind | Message |',
65
+ '|---|---|---|---|',
66
+ ];
67
+ const findings = report.tasks.flatMap((task) => task.findings);
68
+ if (findings.length === 0) {
69
+ lines.push('| - | - | - | No drift detected |');
70
+ }
71
+ else {
72
+ for (const finding of findings) {
73
+ lines.push(`| \`${finding.taskId}\` | ${finding.severity} | ${finding.kind} | ${escapeMarkdownTable(finding.message)} |`);
74
+ }
75
+ }
76
+ if (report.patches.length > 0) {
77
+ lines.push('', '## Patches', '', '| Task | Action | Path |', '|---|---|---|');
78
+ for (const patch of report.patches) {
79
+ lines.push(`| \`${patch.taskId}\` | ${escapeMarkdownTable(patch.action)} | \`${patch.path}\` |`);
80
+ }
81
+ }
82
+ return `${lines.join('\n')}\n`;
83
+ }
84
+ async function listMatchingTaskIds(repoRoot, glob) {
85
+ const tasksRoot = path.join(repoRoot, TASKS_DIR);
86
+ let entries;
87
+ try {
88
+ entries = await readdir(tasksRoot, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ return [];
92
+ }
93
+ return entries
94
+ .filter((entry) => entry.isDirectory())
95
+ .map((entry) => entry.name)
96
+ .filter((taskId) => matchGlob(glob, taskId))
97
+ .sort((left, right) => left.localeCompare(right));
98
+ }
99
+ async function reconcileOneTask(repoRoot, taskId, dagRuns, oneShotRuns, misplacedArtifacts, patchMode, patches) {
100
+ const findings = [];
101
+ const paths = getTaskPaths(repoRoot, taskId);
102
+ const rawTask = await readJsonFile(paths.taskConfigPath);
103
+ const workflow = await readJsonFile(paths.statePath);
104
+ const taskJsonStatus = typeof rawTask?.status === 'string' ? rawTask.status : null;
105
+ const workflowStatus = isWorkflowStatus(workflow?.status) ? workflow.status : null;
106
+ if (!workflowStatus) {
107
+ findings.push(finding(taskId, 'error', 'state-drift', 'missing or unreadable .workflow_state.json status', [relative(repoRoot, paths.statePath)]));
108
+ }
109
+ if (taskJsonStatus && workflowStatus && taskJsonStatus !== workflowStatus) {
110
+ findings.push(finding(taskId, 'warning', 'state-drift', `task.json.status (${taskJsonStatus}) differs from workflow status (${workflowStatus})`, [relative(repoRoot, paths.taskConfigPath), relative(repoRoot, paths.statePath)]));
111
+ if (patchMode) {
112
+ await patchTaskJsonStatus(paths.taskConfigPath, workflowStatus);
113
+ patches.push({ taskId, action: `set task.json.status to ${workflowStatus}`, path: relative(repoRoot, paths.taskConfigPath) });
114
+ }
115
+ }
116
+ await checkArtifacts(repoRoot, taskId, findings, patchMode, patches);
117
+ await checkDagRuns(repoRoot, taskId, workflowStatus, dagRuns, findings);
118
+ await checkOneShotRuns(repoRoot, taskId, workflowStatus, oneShotRuns, findings);
119
+ checkMisplacedArtifacts(repoRoot, taskId, misplacedArtifacts, findings);
120
+ await checkVerification(repoRoot, taskId, findings);
121
+ return {
122
+ taskId,
123
+ status: { taskJsonStatus, workflowStatus },
124
+ findings,
125
+ };
126
+ }
127
+ async function checkOneShotRuns(repoRoot, taskId, workflowStatus, oneShotRuns, findings) {
128
+ const related = oneShotRuns.filter((run) => oneShotMentionsTask(run, taskId));
129
+ if (related.length === 0)
130
+ return;
131
+ const paths = getTaskPaths(repoRoot, taskId);
132
+ const modifyPath = getArtifactPath(paths.taskDir, 'implement');
133
+ const verifyPath = getArtifactPath(paths.taskDir, 'verify');
134
+ const needsPromotion = await artifactNeedsPromotion(modifyPath, verifyPath);
135
+ for (const run of related) {
136
+ if (run.lifecycle === 'active') {
137
+ findings.push(finding(taskId, 'warning', 'run-drift', `active one-shot run ${run.runId} is ${run.record.status}; reconcile or archive it`, [relative(repoRoot, run.runDir)]));
138
+ continue;
139
+ }
140
+ if (run.lifecycle === 'failed' && workflowStatus === 'completed') {
141
+ findings.push(finding(taskId, 'error', 'run-drift', `failed one-shot run ${run.runId} exists for completed task`, [relative(repoRoot, run.runDir)]));
142
+ continue;
143
+ }
144
+ if (run.lifecycle === 'completed' && workflowStatus && workflowStatus !== 'completed') {
145
+ findings.push(finding(taskId, 'warning', 'run-drift', `completed one-shot run ${run.runId} exists while task status is ${workflowStatus}`, [relative(repoRoot, run.runDir)]));
146
+ }
147
+ if (run.lifecycle === 'completed' && needsPromotion) {
148
+ findings.push(finding(taskId, 'warning', 'promotion-drift', `completed one-shot run ${run.runId} has not been promoted to task artifacts; run: npm run dev -- promote-run ${taskId} --run-id ${run.runId}`, [relative(repoRoot, run.runDir), relative(repoRoot, modifyPath), relative(repoRoot, verifyPath)]));
149
+ }
150
+ }
151
+ }
152
+ function checkMisplacedArtifacts(repoRoot, taskId, misplacedArtifacts, findings) {
153
+ if (misplacedArtifacts.length === 0)
154
+ return;
155
+ findings.push(finding(taskId, 'error', 'artifact-drift', './artifacts contains misplaced runtime artifacts; DAG Cursor artifacts must live under .harness/dag-runs/<state>/<run-id>/artifacts/<node-id>/', misplacedArtifacts.map((artifact) => relative(repoRoot, artifact))));
156
+ }
157
+ async function checkArtifacts(repoRoot, taskId, findings, patchMode, patches) {
158
+ const paths = getTaskPaths(repoRoot, taskId);
159
+ const requirementPath = path.join(paths.sourceDir, '需求.md');
160
+ if (!(await fileExists(requirementPath))) {
161
+ findings.push(finding(taskId, 'error', 'artifact-drift', 'missing source/需求.md', [relative(repoRoot, requirementPath)]));
162
+ if (patchMode) {
163
+ await mkdir(path.dirname(requirementPath), { recursive: true });
164
+ await writeFile(requirementPath, '# 需求\n\n<!-- TODO: fill task requirements -->\n', 'utf-8');
165
+ patches.push({ taskId, action: 'created missing requirement skeleton', path: relative(repoRoot, requirementPath) });
166
+ }
167
+ }
168
+ const constraintsPath = path.join(paths.sourceDir, '执行约束.md');
169
+ if (await fileExists(constraintsPath)) {
170
+ const constraints = await readFile(constraintsPath, 'utf-8');
171
+ if (!constraints.trim()) {
172
+ findings.push(finding(taskId, 'warning', 'artifact-drift', 'source/执行约束.md exists but is empty', [relative(repoRoot, constraintsPath)]));
173
+ if (patchMode) {
174
+ await writeFile(constraintsPath, '# 执行约束\n\n<!-- TODO: fill execution constraints -->\n', 'utf-8');
175
+ patches.push({ taskId, action: 'created execution constraint skeleton', path: relative(repoRoot, constraintsPath) });
176
+ }
177
+ }
178
+ }
179
+ for (const artifact of REQUIRED_ARTIFACTS) {
180
+ const artifactPath = artifact.step
181
+ ? getArtifactPath(paths.taskDir, artifact.step)
182
+ : path.join(paths.taskDir, 'artifacts', artifact.fileName);
183
+ if (!(await fileExists(artifactPath))) {
184
+ findings.push(finding(taskId, 'warning', 'artifact-drift', `missing ${artifact.label} artifact`, [relative(repoRoot, artifactPath)]));
185
+ if (patchMode) {
186
+ await mkdir(path.dirname(artifactPath), { recursive: true });
187
+ await writeFile(artifactPath, `# ${artifact.fileName.replace(/\.md$/, '')}\n\n<!-- TODO: fill real ${artifact.label}; this is not verification evidence -->\n`, 'utf-8');
188
+ patches.push({ taskId, action: `created ${artifact.label} skeleton`, path: relative(repoRoot, artifactPath) });
189
+ }
190
+ continue;
191
+ }
192
+ const content = await readFile(artifactPath, 'utf-8');
193
+ const messages = artifact.step
194
+ ? validateArtifactContent(artifact.step, content)
195
+ : validateVerifyArtifact(content);
196
+ if (messages.length > 0) {
197
+ findings.push(finding(taskId, 'warning', 'artifact-drift', `${artifact.label} artifact is not substantive: ${messages.join('; ')}`, [relative(repoRoot, artifactPath)]));
198
+ }
199
+ }
200
+ }
201
+ async function checkDagRuns(repoRoot, taskId, workflowStatus, dagRuns, findings) {
202
+ const related = dagRuns.filter((run) => dagRunMentionsTask(run, taskId));
203
+ const paths = getTaskPaths(repoRoot, taskId);
204
+ const needsPromotion = await artifactNeedsPromotion(getArtifactPath(paths.taskDir, 'implement'), getArtifactPath(paths.taskDir, 'verify'));
205
+ for (const run of related) {
206
+ if (!run.state)
207
+ continue;
208
+ if (run.lifecycle === 'completed' && run.state.status === 'finished' && workflowStatus && workflowStatus !== 'completed') {
209
+ findings.push(finding(taskId, 'warning', 'dag-run-drift', `completed DAG run ${run.runId} finished while task status is ${workflowStatus}`, [relative(repoRoot, run.runDir)]));
210
+ }
211
+ if (run.lifecycle === 'completed' && run.state.status === 'finished' && needsPromotion) {
212
+ findings.push(finding(taskId, 'warning', 'promotion-drift', `completed DAG run ${run.runId} has not been promoted to task artifacts; run: npm run dev -- promote-run ${taskId} --run-id ${run.runId}`, [relative(repoRoot, run.runDir)]));
213
+ }
214
+ if (run.state.status === 'failed' && workflowStatus === 'completed') {
215
+ findings.push(finding(taskId, 'error', 'dag-run-drift', `DAG run ${run.runId} failed while task is completed`, [relative(repoRoot, run.runDir)]));
216
+ }
217
+ if ((run.lifecycle === 'active' || run.lifecycle === 'paused') && workflowStatus === 'completed') {
218
+ findings.push(finding(taskId, 'warning', 'dag-run-drift', `${run.lifecycle} DAG run ${run.runId} exists for completed task`, [relative(repoRoot, run.runDir)]));
219
+ }
220
+ }
221
+ }
222
+ async function checkVerification(repoRoot, taskId, findings) {
223
+ const paths = getTaskPaths(repoRoot, taskId);
224
+ const verifyPath = path.join(paths.taskDir, 'artifacts', '验证结果.md');
225
+ const verifyStat = await stat(verifyPath).catch(() => null);
226
+ const verifyContent = verifyStat ? await readFile(verifyPath, 'utf-8') : '';
227
+ const hasVerifyArtifact = validateVerifyArtifact(verifyContent).length === 0;
228
+ const executorVerify = await latestExecutorVerifyTimestamp(path.join(paths.logsDir, 'executor.jsonl'));
229
+ const dagVerify = await latestDagShellVerifyTimestamp(repoRoot, taskId);
230
+ const latestVerifyMs = Math.max(verifyStat?.mtimeMs ?? 0, executorVerify ?? 0, dagVerify ?? 0);
231
+ if (!hasVerifyArtifact && !executorVerify && !dagVerify) {
232
+ findings.push(finding(taskId, 'warning', 'verification-drift', 'missing final verify / CI / check-repo evidence', [relative(repoRoot, verifyPath), relative(repoRoot, path.join(paths.logsDir, 'executor.jsonl'))]));
233
+ }
234
+ const closeout = await latestCloseoutTimestamp(paths.taskDir);
235
+ if (closeout && latestVerifyMs > 0 && latestVerifyMs < closeout.mtimeMs) {
236
+ findings.push(finding(taskId, 'warning', 'verification-drift', 'latest verification evidence is older than closeout artifact', [relative(repoRoot, closeout.path)]));
237
+ }
238
+ }
239
+ async function loadDagRunFacts(repoRoot) {
240
+ const entries = await listAllDagRunEntries(repoRoot);
241
+ const facts = [];
242
+ for (const entry of entries) {
243
+ const state = await readDagRunState(entry.runDir).catch(() => null);
244
+ const spec = await readDagRunSpec(entry.runDir).catch(() => null);
245
+ facts.push({
246
+ ...entry,
247
+ state,
248
+ specText: spec ? JSON.stringify(spec) : '',
249
+ });
250
+ }
251
+ return facts;
252
+ }
253
+ async function loadOneShotRunFacts(repoRoot) {
254
+ const facts = [];
255
+ for (const lifecycle of ['active', 'completed', 'failed']) {
256
+ const dir = path.join(repoRoot, '.harness', 'runs', lifecycle);
257
+ const entries = await readdir(dir).catch(() => []);
258
+ for (const entry of entries) {
259
+ if (entry === '.gitkeep' || entry === '.DS_Store')
260
+ continue;
261
+ const entryPath = path.join(dir, entry);
262
+ const logPath = entry.endsWith('.md') ? entryPath : path.join(entryPath, 'run.md');
263
+ const content = await readFile(logPath, 'utf-8').catch(() => '');
264
+ if (!content)
265
+ continue;
266
+ const record = parseCursorRunLog(content, logPath);
267
+ record.status = resolveRunStatus(record, lifecycle === 'completed', undefined, lifecycle === 'failed');
268
+ facts.push({
269
+ runId: entry.endsWith('.md') ? entry.replace(/\.md$/, '') : entry,
270
+ lifecycle,
271
+ runDir: entryPath,
272
+ record,
273
+ });
274
+ }
275
+ }
276
+ return facts;
277
+ }
278
+ async function listMisplacedCodeAgentArtifacts(repoRoot) {
279
+ const root = path.join(repoRoot, 'tools', 'loop-agent', 'artifacts');
280
+ const results = [];
281
+ await collectFiles(root, results);
282
+ return results;
283
+ }
284
+ async function collectFiles(dir, results) {
285
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
286
+ for (const entry of entries) {
287
+ const entryPath = path.join(dir, entry.name);
288
+ if (entry.isDirectory()) {
289
+ await collectFiles(entryPath, results);
290
+ }
291
+ else if (entry.name !== '.gitkeep') {
292
+ results.push(entryPath);
293
+ }
294
+ }
295
+ }
296
+ function dagRunMentionsTask(run, taskId) {
297
+ const title = run.state?.title ?? '';
298
+ return run.runId.includes(taskId)
299
+ || title.includes(taskId)
300
+ || run.specText.includes(`taskId: ${taskId}`)
301
+ || run.specText.includes(`"taskId":"${taskId}"`)
302
+ || run.specText.includes(taskId);
303
+ }
304
+ function oneShotMentionsTask(run, taskId) {
305
+ return run.runId.includes(taskId)
306
+ || run.record.taskId === taskId
307
+ || run.record.task.includes(taskId)
308
+ || run.record.logPath.includes(taskId);
309
+ }
310
+ async function artifactNeedsPromotion(modifyPath, verifyPath) {
311
+ const modify = await readFile(modifyPath, 'utf-8').catch(() => '');
312
+ const verify = await readFile(verifyPath, 'utf-8').catch(() => '');
313
+ return validateArtifactContent('implement', modify).length > 0 || validateVerifyArtifact(verify).length > 0;
314
+ }
315
+ async function latestExecutorVerifyTimestamp(logPath) {
316
+ const raw = await readFile(logPath, 'utf-8').catch(() => '');
317
+ let latest = 0;
318
+ for (const line of raw.split(/\r?\n/)) {
319
+ if (!line.trim())
320
+ continue;
321
+ try {
322
+ const record = JSON.parse(line);
323
+ if (record.step === 'verify' && (record.ok === true || record.exitCode === 0) && typeof record.timestamp === 'string') {
324
+ const time = Date.parse(record.timestamp);
325
+ if (!Number.isNaN(time))
326
+ latest = Math.max(latest, time);
327
+ }
328
+ }
329
+ catch {
330
+ // Ignore malformed log lines; reconcile reports missing evidence if nothing valid exists.
331
+ }
332
+ }
333
+ return latest || null;
334
+ }
335
+ async function latestDagShellVerifyTimestamp(repoRoot, taskId) {
336
+ const runs = await loadDagRunFacts(repoRoot);
337
+ let latest = 0;
338
+ for (const run of runs.filter((candidate) => dagRunMentionsTask(candidate, taskId))) {
339
+ for (const [nodeId, node] of Object.entries(run.state?.nodes ?? {})) {
340
+ if (!/verify|ci|check/i.test(nodeId))
341
+ continue;
342
+ if (node.status !== 'FINISHED' || node.failureCategory !== 'success')
343
+ continue;
344
+ const time = typeof node.finishedAt === 'string' ? Date.parse(node.finishedAt) : 0;
345
+ if (!Number.isNaN(time))
346
+ latest = Math.max(latest, time);
347
+ }
348
+ }
349
+ return latest || null;
350
+ }
351
+ async function latestCloseoutTimestamp(taskDir) {
352
+ const artifactsDir = path.join(taskDir, 'artifacts');
353
+ let latest = null;
354
+ for (const name of CLOSEOUT_NAMES) {
355
+ const candidate = path.join(artifactsDir, name);
356
+ const candidateStat = await stat(candidate).catch(() => null);
357
+ if (!candidateStat)
358
+ continue;
359
+ if (!latest || candidateStat.mtimeMs > latest.mtimeMs) {
360
+ latest = { path: candidate, mtimeMs: candidateStat.mtimeMs };
361
+ }
362
+ }
363
+ return latest;
364
+ }
365
+ async function patchTaskJsonStatus(taskConfigPath, status) {
366
+ const raw = await readJsonFile(taskConfigPath);
367
+ const next = { ...(raw ?? {}), status };
368
+ await writeFile(taskConfigPath, `${JSON.stringify(next, null, 2)}\n`, 'utf-8');
369
+ }
370
+ function validateVerifyArtifact(content) {
371
+ const trimmed = content.trim();
372
+ if (!trimmed)
373
+ return ['artifact content is empty'];
374
+ if (TEMPLATE_MARKERS.some((marker) => marker.test(trimmed))) {
375
+ return ['artifact contains template marker'];
376
+ }
377
+ if (!VERIFY_MARKERS.some((marker) => marker.test(trimmed))) {
378
+ return ['artifact does not mention verification commands or CI evidence'];
379
+ }
380
+ return [];
381
+ }
382
+ async function readJsonFile(filePath) {
383
+ try {
384
+ return JSON.parse(await readFile(filePath, 'utf-8'));
385
+ }
386
+ catch {
387
+ return null;
388
+ }
389
+ }
390
+ async function fileExists(filePath) {
391
+ return Boolean(await stat(filePath).catch(() => null));
392
+ }
393
+ function isWorkflowStatus(value) {
394
+ return value === 'draft' || value === 'active' || value === 'completed' || value === 'failed';
395
+ }
396
+ function finding(taskId, severity, kind, message, evidence) {
397
+ return { taskId, severity, kind, message, evidence };
398
+ }
399
+ function relative(repoRoot, target) {
400
+ return path.relative(repoRoot, target).replace(/\\/g, '/');
401
+ }
402
+ function escapeMarkdownTable(value) {
403
+ return value.replace(/\|/g, '\\|').replace(/\n/g, ' ');
404
+ }