@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,135 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const NEGATION_PATTERN = /\b(?:do not|don't|must not|never|cannot|should not|not|no|without|禁止|不得|不要|不能|不可)\b|不\s*(?:mutate|修改|重写|删除|覆盖|绕过|跳过)/i;
4
+ const CREDENTIAL_PATTERN = /(?:api[\s_-]?key|token|password|credential|secret|密钥|令牌|凭据|密码)/i;
5
+ const SKILL_SAFETY_RULES = [
6
+ {
7
+ id: "learned-shell-command",
8
+ message: "learned guidance must not add shell or runtime commands",
9
+ profiles: ["learned"],
10
+ patterns: [
11
+ /^```/,
12
+ /^\s*\$\s*\S+/,
13
+ /^\s*(?:bash|sh|zsh|npm|pnpm|yarn|node|python|python3|curl|chmod|sudo|git)\b/i,
14
+ ],
15
+ },
16
+ {
17
+ id: "credential-handling",
18
+ message: "skill guidance must not request credential handling or disclosure",
19
+ profiles: ["policy", "learned"],
20
+ patterns: [
21
+ new RegExp(`\\b(?:read|print|echo|export|send|upload|store|ask\\s+for|prompt\\s+for|collect|handle|rotate|change)\\b.{0,48}${CREDENTIAL_PATTERN.source}`, "i"),
22
+ new RegExp(`${CREDENTIAL_PATTERN.source}.{0,48}\\b(?:read|print|echo|export|send|upload|store|ask\\s+for|prompt\\s+for|collect|handle|rotate|change)\\b`, "i"),
23
+ ],
24
+ },
25
+ {
26
+ id: "permission-expansion",
27
+ message: "skill guidance must not expand tools, permissions, or path allowlists",
28
+ profiles: ["policy", "learned"],
29
+ patterns: [
30
+ /\b(?:grant|expand|enable|add|relax|broaden)\b.{0,40}\b(?:tools?|permissions?|allowlists?|allowedPaths|forbiddenPaths|writeSet|权限|工具|白名单)\b/i,
31
+ /\b--tools\b.{0,40}\b(?:write|shell|exec|all)\b/i,
32
+ ],
33
+ },
34
+ {
35
+ id: "completion-authority-bypass",
36
+ message: "skill guidance must not bypass verification, gates, or completion authority",
37
+ profiles: ["policy", "learned"],
38
+ patterns: [
39
+ /\b(?:skip|bypass|disable|ignore)\b.{0,40}\b(?:verification|tests?|guards?|gates?|human gate|decision gate|write guard|shell verification|验证|测试|门禁)\b/i,
40
+ /\bcomplete(?:d|ion)?\b.{0,40}\bwithout\b.{0,40}\b(?:verification|tests?|evidence|gate)\b/i,
41
+ /\b(?:always|auto(?:matically)?)\b.{0,40}\b(?:accept|approve|complete|mark complete)\b/i,
42
+ ],
43
+ },
44
+ {
45
+ id: "completed-facts-mutation",
46
+ message: "skill guidance must not mutate completed DAG or one-shot run facts",
47
+ profiles: ["policy", "learned"],
48
+ patterns: [
49
+ /\b(?:modify|rewrite|edit|delete|overwrite|mutate)\b.{0,60}\b(?:completed DAG|completed run|one-shot run|run facts|dag-runs\/completed|runs\/completed)\b/i,
50
+ ],
51
+ },
52
+ ];
53
+ function toRepoRelative(repoRoot, filePath) {
54
+ return path.relative(repoRoot, filePath).split(path.sep).join("/");
55
+ }
56
+ function isMarkdownFile(filePath) {
57
+ return filePath.endsWith(".md");
58
+ }
59
+ async function listMarkdownFiles(root) {
60
+ const files = [];
61
+ async function walk(dir) {
62
+ let entries;
63
+ try {
64
+ entries = await readdir(dir, { withFileTypes: true });
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ for (const entry of entries) {
70
+ const full = path.join(dir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ await walk(full);
73
+ continue;
74
+ }
75
+ if (entry.isFile() && isMarkdownFile(full)) {
76
+ files.push(full);
77
+ }
78
+ }
79
+ }
80
+ await walk(root);
81
+ return files.sort();
82
+ }
83
+ function profileForFile(filePath) {
84
+ if (path.basename(filePath) === "prompt-delta.md")
85
+ return "learned";
86
+ return filePath.split(path.sep).includes("learned") ? "learned" : "policy";
87
+ }
88
+ function shouldIgnoreLine(line) {
89
+ return NEGATION_PATTERN.test(line);
90
+ }
91
+ export function auditSkillMarkdownContent(input) {
92
+ const profile = input.profile ?? profileForFile(input.filePath);
93
+ const relative = toRepoRelative(input.repoRoot, input.filePath);
94
+ const findings = [];
95
+ input.content.split(/\r?\n/).forEach((line, index) => {
96
+ if (shouldIgnoreLine(line))
97
+ return;
98
+ for (const rule of SKILL_SAFETY_RULES) {
99
+ if (!rule.profiles.includes(profile))
100
+ continue;
101
+ if (!rule.patterns.some((pattern) => pattern.test(line)))
102
+ continue;
103
+ findings.push({
104
+ ruleId: rule.id,
105
+ severity: "error",
106
+ file: relative,
107
+ line: index + 1,
108
+ snippet: line.trim().slice(0, 160),
109
+ message: rule.message,
110
+ });
111
+ }
112
+ });
113
+ return findings;
114
+ }
115
+ export async function auditLoopAgentSkillSafety(repoRoot) {
116
+ const skillRoot = path.join(repoRoot, "skills", "loop-agent");
117
+ const harnessTasksRoot = path.join(repoRoot, ".harness", "tasks");
118
+ const skillFiles = await listMarkdownFiles(skillRoot);
119
+ const promptDeltaFiles = (await listMarkdownFiles(harnessTasksRoot)).filter((filePath) => path.basename(filePath) === "prompt-delta.md" &&
120
+ filePath.split(path.sep).includes("source"));
121
+ const files = [...skillFiles, ...promptDeltaFiles].sort();
122
+ const findings = [];
123
+ for (const filePath of files) {
124
+ const content = await readFile(filePath, "utf-8");
125
+ findings.push(...auditSkillMarkdownContent({ repoRoot, filePath, content }));
126
+ }
127
+ return {
128
+ root: `${toRepoRelative(repoRoot, skillRoot)} + .harness/tasks/*/source/prompt-delta.md`,
129
+ filesChecked: files.length,
130
+ findings,
131
+ summary: {
132
+ errorCount: findings.length,
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,152 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveAdapter } from "../adapters/index.js";
4
+ import { resolveVerifyPreset } from "../executors/shell-verification.js";
5
+ import { pathMatchesPattern } from "../shared/git-progress.js";
6
+ import { getTaskPaths, loadTaskConfig } from "../task/runtime.js";
7
+ import { formatRequirementCoverageMarkdown, requirementCoverageFailureReasons, runRequirementCoverageAudit, } from "./requirement-coverage.js";
8
+ async function exists(filePath) {
9
+ try {
10
+ await access(filePath);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function toRepoRelative(repoRoot, filePath) {
18
+ return path.relative(repoRoot, filePath).split(path.sep).join("/");
19
+ }
20
+ function probeForPattern(pattern) {
21
+ return pattern
22
+ .replace(/\\/g, "/")
23
+ .replace(/\/\*\*$/, "/_probe_")
24
+ .replace(/\*\*$/, "_probe_")
25
+ .replace(/\*/g, "_")
26
+ .replace(/^\.\//, "");
27
+ }
28
+ function patternsOverlap(left, right) {
29
+ const leftProbe = probeForPattern(left);
30
+ const rightProbe = probeForPattern(right);
31
+ return (pathMatchesPattern(leftProbe, right) ||
32
+ pathMatchesPattern(rightProbe, left) ||
33
+ leftProbe === rightProbe);
34
+ }
35
+ function isBroadAllowedPath(pattern) {
36
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
37
+ return normalized === "**" || normalized === "./**" || normalized === ".";
38
+ }
39
+ export async function runSpineAudit(repoRoot, taskId) {
40
+ const taskPaths = getTaskPaths(repoRoot, taskId);
41
+ const config = await loadTaskConfig(repoRoot, taskId);
42
+ const requirementPath = path.join(taskPaths.sourceDir, "需求.md");
43
+ const constraintsPath = path.join(taskPaths.sourceDir, "执行约束.md");
44
+ const findings = [];
45
+ if (!(await exists(requirementPath))) {
46
+ findings.push({
47
+ severity: "error",
48
+ type: "missing-source",
49
+ message: "missing source/需求.md",
50
+ evidence: [toRepoRelative(repoRoot, requirementPath)],
51
+ });
52
+ }
53
+ if (!(await exists(constraintsPath))) {
54
+ findings.push({
55
+ severity: "error",
56
+ type: "missing-source",
57
+ message: "missing source/执行约束.md",
58
+ evidence: [toRepoRelative(repoRoot, constraintsPath)],
59
+ });
60
+ }
61
+ if ((config.complexity === "medium" || config.complexity === "large") && config.allowedPaths.length === 0) {
62
+ findings.push({
63
+ severity: "error",
64
+ type: "empty-allowed-paths",
65
+ message: `${config.complexity} task requires non-empty allowedPaths`,
66
+ });
67
+ }
68
+ for (const allowed of config.allowedPaths) {
69
+ if (isBroadAllowedPath(allowed)) {
70
+ findings.push({
71
+ severity: config.complexity === "small" ? "warning" : "error",
72
+ type: "broad-allowed-paths",
73
+ message: `allowedPaths entry "${allowed}" is too broad for a spine-owned task`,
74
+ });
75
+ }
76
+ for (const forbidden of config.forbiddenPaths) {
77
+ if (patternsOverlap(allowed, forbidden)) {
78
+ findings.push({
79
+ severity: "error",
80
+ type: "allowed-forbidden-overlap",
81
+ message: `allowedPaths entry "${allowed}" overlaps forbiddenPaths entry "${forbidden}"`,
82
+ });
83
+ }
84
+ }
85
+ }
86
+ const requirementCoverage = await runRequirementCoverageAudit(repoRoot, taskId);
87
+ for (const reason of requirementCoverageFailureReasons(requirementCoverage)) {
88
+ findings.push({
89
+ severity: "error",
90
+ type: "uncovered-requirement",
91
+ message: reason,
92
+ });
93
+ }
94
+ const adapter = await resolveAdapter(repoRoot);
95
+ const preset = resolveVerifyPreset(config.verifyPreset, config);
96
+ const verifyCommands = adapter.getVerifyCommands(repoRoot, {
97
+ preset,
98
+ quota: "full",
99
+ phase: "final",
100
+ taskConfig: config,
101
+ });
102
+ if (verifyCommands.length === 0) {
103
+ findings.push({
104
+ severity: "error",
105
+ type: "missing-verification",
106
+ message: "adapter returned no final verification commands",
107
+ });
108
+ }
109
+ return {
110
+ ok: findings.every((finding) => finding.severity !== "error"),
111
+ taskId,
112
+ extractedAt: new Date().toISOString(),
113
+ objectiveSource: toRepoRelative(repoRoot, requirementPath),
114
+ constraintsSource: toRepoRelative(repoRoot, constraintsPath),
115
+ allowedPaths: config.allowedPaths,
116
+ forbiddenPaths: config.forbiddenPaths,
117
+ verifyCommandLabels: verifyCommands.map((command) => command.label),
118
+ requirementCoverage,
119
+ findings,
120
+ };
121
+ }
122
+ export function formatSpineAuditMarkdown(audit) {
123
+ const lines = [
124
+ "# Minimal Spec Spine Audit",
125
+ "",
126
+ `- task: \`${audit.taskId}\``,
127
+ `- extractedAt: ${audit.extractedAt}`,
128
+ `- ok: ${audit.ok ? "yes" : "no"}`,
129
+ `- objective source: \`${audit.objectiveSource}\``,
130
+ `- constraints source: \`${audit.constraintsSource}\``,
131
+ `- allowedPaths: ${audit.allowedPaths.length > 0 ? audit.allowedPaths.join(", ") : "(empty)"}`,
132
+ `- forbiddenPaths: ${audit.forbiddenPaths.length > 0 ? audit.forbiddenPaths.join(", ") : "(empty)"}`,
133
+ `- final verification: ${audit.verifyCommandLabels.length > 0 ? audit.verifyCommandLabels.join(", ") : "(empty)"}`,
134
+ "",
135
+ "## Findings",
136
+ "",
137
+ ];
138
+ if (audit.findings.length === 0) {
139
+ lines.push("- none", "");
140
+ }
141
+ else {
142
+ for (const finding of audit.findings) {
143
+ lines.push(`- ${finding.severity}: ${finding.type}: ${finding.message}`);
144
+ }
145
+ lines.push("");
146
+ }
147
+ lines.push(formatRequirementCoverageMarkdown(audit.requirementCoverage).trimEnd());
148
+ return `${lines.join("\n")}\n`;
149
+ }
150
+ export function formatSpineAuditJson(audit) {
151
+ return `${JSON.stringify(audit, null, 2)}\n`;
152
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./promotion.js";
2
+ export * from "../commands/closeout.js";
@@ -0,0 +1,236 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { loadHarnessManifest } from '../governance/harness.js';
5
+ import { resolveWorktreePath } from '../task/worktree.js';
6
+ import { getTaskPaths, getTaskStatus, loadTaskConfig } from '../task/runtime.js';
7
+ import { validateDiffPathGuard, listDiffPaths } from '../governance/path-guard.js';
8
+ import { validateCursorArtifacts } from '../executors/cursor-artifacts.js';
9
+ import { checkTaskHandoff } from '../governance/checks.js';
10
+ import { archiveTaskDirFromWorktree } from '../task/worktree-cleanup.js';
11
+ export { copyDir } from '../shared/copy-dir.js';
12
+ /**
13
+ * Harvest a completed task: validate state → preview diff → merge → archive → cleanup.
14
+ *
15
+ * Refuses to harvest unless task status is "completed". Failed/active tasks must
16
+ * be inspected manually; their worktree is preserved.
17
+ */
18
+ export async function harvestTask(repoRoot, manifest, taskId, opts = {}) {
19
+ const archive = opts.archive !== false;
20
+ const keepWorktree = opts.keepWorktree === true;
21
+ const squash = opts.squash === true;
22
+ // Step 1: Locate worktree
23
+ let effectiveManifest = manifest;
24
+ if (!effectiveManifest) {
25
+ try {
26
+ effectiveManifest = await loadHarnessManifest(repoRoot);
27
+ }
28
+ catch {
29
+ effectiveManifest = undefined;
30
+ }
31
+ }
32
+ const worktreePath = resolveWorktreePath(repoRoot, effectiveManifest, taskId);
33
+ const taskRoot = await resolveHarvestTaskRoot(repoRoot, worktreePath, taskId);
34
+ const worktreePresent = (await pathExists(worktreePath)) && (await pathExists(path.join(worktreePath, '.git')));
35
+ // Step 2: Preflight — task must be completed (prefer archived main-repo state after cleanup)
36
+ let state;
37
+ try {
38
+ state = await getTaskStatus(taskRoot, taskId);
39
+ }
40
+ catch (err) {
41
+ const msg = err instanceof Error ? err.message : String(err);
42
+ throw new Error(`harvest preflight failed: cannot load task state for "${taskId}": ${msg}`);
43
+ }
44
+ if (state.status !== 'completed') {
45
+ throw new Error(`harvest refused: task "${taskId}" status is "${state.status}", expected "completed". ` +
46
+ 'Failed/active tasks must be inspected manually; their worktree is preserved.');
47
+ }
48
+ // Step 3: Determine task branch (worktree cwd or default task/<id> after cleanup)
49
+ let branch = `task/${taskId}`;
50
+ if (worktreePresent) {
51
+ branch = (await tryRunGit(repoRoot, ['-C', worktreePath, 'branch', '--show-current']))?.trim() ?? branch;
52
+ }
53
+ const baseBranch = (await tryRunGit(repoRoot, ['branch', '--show-current']))?.trim() || 'main';
54
+ // Step 4: Generate diff preview (commits + diffstat)
55
+ const commitsRaw = (await tryRunGit(repoRoot, ['log', `${baseBranch}..${branch}`, '--oneline'])) ?? '';
56
+ const diffstat = (await tryRunGit(repoRoot, ['diff', '--stat', `${baseBranch}..${branch}`])) ?? '';
57
+ const commits = commitsRaw.split('\n').map((line) => line.trim()).filter(Boolean);
58
+ if (commits.length === 0) {
59
+ throw new Error(`harvest aborted: branch "${branch}" has no new commits relative to "${baseBranch}". ` +
60
+ 'Either the task did nothing, or the base branch already includes its work.');
61
+ }
62
+ // Step 4b: Path / artifact / handoff gates
63
+ await runHarvestGates(repoRoot, taskId, taskRoot, baseBranch, branch);
64
+ // Step 5: Merge into base branch
65
+ let commitHash;
66
+ let merged = false;
67
+ try {
68
+ if (squash) {
69
+ await runGit(repoRoot, ['merge', '--squash', branch]);
70
+ await runGit(repoRoot, ['commit', '-m', `feat(task): merge ${taskId} (squashed)`]);
71
+ }
72
+ else {
73
+ await runGit(repoRoot, ['merge', '--no-ff', branch, '-m', `feat(task): merge ${taskId}`]);
74
+ }
75
+ merged = true;
76
+ commitHash = (await tryRunGit(repoRoot, ['rev-parse', 'HEAD']))?.trim();
77
+ }
78
+ catch (err) {
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ // Try to detect conflict and surface helpful info
81
+ const conflictFiles = await detectConflictFiles(repoRoot);
82
+ throw new Error(`harvest merge failed: ${msg}` +
83
+ (conflictFiles.length > 0
84
+ ? `\nConflicting files:\n - ${conflictFiles.join('\n - ')}\nResolve manually then re-run harvest.`
85
+ : ''));
86
+ }
87
+ // Step 6: Archive — copy artifacts/logs/state from worktree back to main repo
88
+ let archived = false;
89
+ if (archive) {
90
+ try {
91
+ if (await pathExists(worktreePath)) {
92
+ await archiveTaskDirFromWorktree(repoRoot, worktreePath, taskId);
93
+ }
94
+ archived = true;
95
+ }
96
+ catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ throw new Error(`harvest archive failed: ${msg}. Merge already completed; worktree preserved.`);
99
+ }
100
+ }
101
+ // Step 7: Cleanup — remove worktree + branch
102
+ let worktreeRemoved = false;
103
+ let branchDeleted = false;
104
+ if (!keepWorktree) {
105
+ try {
106
+ await runGit(repoRoot, ['worktree', 'remove', worktreePath]);
107
+ worktreeRemoved = true;
108
+ }
109
+ catch {
110
+ // Best-effort. If remove fails, instruct user; do not abort harvest.
111
+ }
112
+ try {
113
+ await runGit(repoRoot, ['branch', '-d', branch]);
114
+ branchDeleted = true;
115
+ }
116
+ catch {
117
+ // Branch may already be deleted by merge or hold unmerged commits; ignore.
118
+ }
119
+ }
120
+ return {
121
+ taskId,
122
+ branch,
123
+ baseBranch,
124
+ worktreePath,
125
+ merged,
126
+ squash,
127
+ commitHash,
128
+ archived,
129
+ worktreeRemoved,
130
+ branchDeleted,
131
+ diff: {
132
+ commits,
133
+ diffstat,
134
+ },
135
+ };
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // Harvest gates (path guard, artifacts, handoff)
139
+ // ---------------------------------------------------------------------------
140
+ async function resolveHarvestTaskRoot(repoRoot, worktreePath, taskId) {
141
+ if (await pathExists(getTaskPaths(worktreePath, taskId).statePath)) {
142
+ return worktreePath;
143
+ }
144
+ if (await pathExists(getTaskPaths(repoRoot, taskId).statePath)) {
145
+ return repoRoot;
146
+ }
147
+ return worktreePath;
148
+ }
149
+ async function runHarvestGates(repoRoot, taskId, taskRoot, baseBranch, branch) {
150
+ let taskConfig;
151
+ try {
152
+ taskConfig = await loadTaskConfig(taskRoot, taskId);
153
+ }
154
+ catch {
155
+ return;
156
+ }
157
+ const shouldRunPathGuard = taskConfig.executor === 'cursor'
158
+ || (taskConfig.allowedPaths ?? []).length > 0
159
+ || (taskConfig.forbiddenPaths ?? []).length > 0;
160
+ if (shouldRunPathGuard) {
161
+ const changedPaths = await listDiffPaths(repoRoot, baseBranch, branch);
162
+ const pathGuard = validateDiffPathGuard(changedPaths, taskConfig);
163
+ if (!pathGuard.ok) {
164
+ throw new Error(`harvest path guard failed for task "${taskId}": ${pathGuard.violations.join(', ')}. ` +
165
+ 'Worktree preserved for manual review.');
166
+ }
167
+ }
168
+ if (taskConfig.executor !== 'cursor') {
169
+ return;
170
+ }
171
+ const paths = getTaskPaths(taskRoot, taskId);
172
+ const modifyLog = await readFile(path.join(paths.taskDir, 'artifacts', '修改记录.md'), 'utf-8');
173
+ const verifyResult = await readFile(path.join(paths.taskDir, 'artifacts', '验证结果.md'), 'utf-8');
174
+ const artifactIssues = validateCursorArtifacts(modifyLog, verifyResult);
175
+ if (artifactIssues.length > 0) {
176
+ throw new Error(`harvest artifact validation failed for cursor task "${taskId}": ${artifactIssues.join('; ')}. ` +
177
+ 'Worktree preserved for manual review.');
178
+ }
179
+ const handoff = await checkTaskHandoff(taskRoot, taskId);
180
+ const taskHandoff = handoff.tasks.find((task) => task.taskId === taskId);
181
+ const blockingIssues = (taskHandoff?.issues ?? []).filter((issue) => issue.severity === 'error');
182
+ if (blockingIssues.length > 0) {
183
+ throw new Error(`harvest handoff check failed for task "${taskId}": ${blockingIssues.map((issue) => issue.message).join('; ')}. ` +
184
+ 'Worktree preserved for manual review.');
185
+ }
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Helpers
189
+ // ---------------------------------------------------------------------------
190
+ async function pathExists(p) {
191
+ try {
192
+ await stat(p);
193
+ return true;
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ }
199
+ async function detectConflictFiles(repoRoot) {
200
+ const out = await tryRunGit(repoRoot, ['diff', '--name-only', '--diff-filter=U']);
201
+ if (!out)
202
+ return [];
203
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
204
+ }
205
+ async function tryRunGit(repoRoot, args) {
206
+ try {
207
+ return await runGit(repoRoot, args);
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
213
+ function runGit(repoRoot, args) {
214
+ return new Promise((resolve, reject) => {
215
+ const child = spawn('git', args, {
216
+ cwd: repoRoot,
217
+ stdio: ['ignore', 'pipe', 'pipe'],
218
+ });
219
+ const stdoutChunks = [];
220
+ const stderrChunks = [];
221
+ child.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
222
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
223
+ child.on('error', (err) => {
224
+ reject(new Error(`git ${args.join(' ')} spawn error: ${err.message}`));
225
+ });
226
+ child.on('close', (code) => {
227
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
228
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8');
229
+ if (code !== 0) {
230
+ reject(new Error(`git ${args.join(' ')} failed (exit ${code}): ${stderr.trim() || stdout.trim()}`));
231
+ return;
232
+ }
233
+ resolve(stdout);
234
+ });
235
+ });
236
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./promotion.js";
2
+ export * from "./closeout.js";
3
+ export * from "./one-shot-runs.js";