@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,421 @@
1
+ import { access, cp, mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { truncateOutput } from '../executors/cursor-executor.js';
4
+ /** Maximum chars for result summary in the run log. */
5
+ export const RUN_LOG_RESULT_MAX = 4_000;
6
+ /** Placeholder written to active logs before execution finishes. */
7
+ export const RUN_LOG_RUNNING_PLACEHOLDER = '_(执行中…)_';
8
+ /**
9
+ * Compute the active run directory under .harness/runs/active/<slug>/.
10
+ * Creates the directory and returns the path.
11
+ */
12
+ export async function computeRunDir(cwd, task) {
13
+ const harnessDir = path.join(cwd, '.harness');
14
+ try {
15
+ await access(harnessDir);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ const datePrefix = new Date().toISOString().slice(0, 10);
21
+ const slug = deriveRunSlug(task);
22
+ const activeDir = path.join(harnessDir, 'runs', 'active');
23
+ await mkdir(activeDir, { recursive: true });
24
+ const runDir = await resolveUniqueRunPath(activeDir, `${datePrefix}-${slug}`);
25
+ await mkdir(runDir, { recursive: true });
26
+ return runDir;
27
+ }
28
+ /**
29
+ * Persist a human-readable run log under `.harness/runs/`.
30
+ *
31
+ * Lifecycle:
32
+ * 1. Write to `.harness/runs/active/<date>-<slug>/run.md`
33
+ * 2. If execution succeeded, move the run directory to `.harness/runs/completed/`
34
+ * 3. If execution failed, move the run directory to `.harness/runs/failed/`
35
+ *
36
+ * No-op when `cwd` does not contain a `.harness/` directory.
37
+ */
38
+ export async function persistCursorRunLog(cwd, input) {
39
+ const harnessDir = path.join(cwd, '.harness');
40
+ try {
41
+ await access(harnessDir);
42
+ }
43
+ catch {
44
+ return null; // not a harness repo — skip silently
45
+ }
46
+ const datePrefix = new Date().toISOString().slice(0, 10);
47
+ const slug = deriveRunSlug(input.task);
48
+ const baseName = `${datePrefix}-${slug}`;
49
+ const activeDir = path.join(harnessDir, 'runs', 'active');
50
+ const completedDir = path.join(harnessDir, 'runs', 'completed');
51
+ const failedDir = path.join(harnessDir, 'runs', 'failed');
52
+ await mkdir(activeDir, { recursive: true });
53
+ await mkdir(completedDir, { recursive: true });
54
+ await mkdir(failedDir, { recursive: true });
55
+ const activeRunDir = input.runDir ?? await resolveUniqueRunPath(activeDir, baseName);
56
+ await mkdir(activeRunDir, { recursive: true });
57
+ const content = buildRunLogMarkdown(input);
58
+ const activePath = path.join(activeRunDir, 'run.md');
59
+ await writeFile(activePath, content, 'utf-8');
60
+ await writeRunMeta(activeRunDir, input);
61
+ if (input.result.ok) {
62
+ const completedPath = await resolveUniqueRunPath(completedDir, path.basename(activeRunDir));
63
+ await moveRunDirectory(activeRunDir, completedPath);
64
+ return { logPath: path.join(completedPath, 'run.md'), completed: true };
65
+ }
66
+ const failedPath = await resolveUniqueRunPath(failedDir, path.basename(activeRunDir));
67
+ if (input.result.status === 'failed' || input.result.status === 'timeout' || input.result.status === 'cancelled') {
68
+ await moveRunDirectory(activeRunDir, failedPath);
69
+ return { logPath: path.join(failedPath, 'run.md'), completed: false };
70
+ }
71
+ return { logPath: activePath, completed: false };
72
+ }
73
+ async function moveRunDirectory(fromDir, toDir) {
74
+ await mkdir(path.dirname(toDir), { recursive: true });
75
+ try {
76
+ await rename(fromDir, toDir);
77
+ }
78
+ catch {
79
+ await cp(fromDir, toDir, { recursive: true });
80
+ await rm(fromDir, { recursive: true, force: true });
81
+ }
82
+ }
83
+ async function writeRunMeta(runDir, input, statusOverride) {
84
+ const runId = path.basename(runDir);
85
+ const status = statusOverride ?? (input.result.ok ? 'completed' : input.result.status);
86
+ await writeFile(path.join(runDir, 'meta.json'), `${JSON.stringify({
87
+ runId,
88
+ taskId: input.taskId,
89
+ status,
90
+ model: input.model,
91
+ channel: input.channel,
92
+ cwd: input.cwd,
93
+ startedDate: new Date().toISOString().slice(0, 10),
94
+ durationMs: input.result.durationMs,
95
+ failureCategory: input.result.failureCategory,
96
+ artifactsDir: 'artifacts',
97
+ logPath: 'run.md',
98
+ }, null, 2)}\n`, 'utf-8');
99
+ }
100
+ function isRunMarkdownPath(logPath) {
101
+ return path.basename(logPath) === 'run.md';
102
+ }
103
+ function runDirForLogPath(logPath) {
104
+ return isRunMarkdownPath(logPath) ? path.dirname(logPath) : '';
105
+ }
106
+ function parseTaskIdFallback(logPath) {
107
+ if (isRunMarkdownPath(logPath))
108
+ return path.basename(path.dirname(logPath));
109
+ return path.basename(logPath, '.md');
110
+ }
111
+ /**
112
+ * Write an in-progress run log under `.harness/runs/active/`.
113
+ * Optional helper for two-phase logging; Pi `cursor` tool uses sync `persistCursorRunLog` instead.
114
+ */
115
+ export async function createActiveCursorRun(cwd, input) {
116
+ const harnessDir = await resolveHarnessDir(cwd);
117
+ if (!harnessDir)
118
+ return null;
119
+ const datePrefix = new Date().toISOString().slice(0, 10);
120
+ const slug = deriveRunSlug(input.task);
121
+ const baseName = `${datePrefix}-${slug}`;
122
+ const activeDir = path.join(harnessDir, 'runs', 'active');
123
+ await mkdir(activeDir, { recursive: true });
124
+ await mkdir(path.join(harnessDir, 'runs', 'completed'), { recursive: true });
125
+ await mkdir(path.join(harnessDir, 'runs', 'failed'), { recursive: true });
126
+ const activeRunDir = await resolveUniqueRunPath(activeDir, baseName);
127
+ await mkdir(activeRunDir, { recursive: true });
128
+ const activePath = path.join(activeRunDir, 'run.md');
129
+ const content = buildActiveRunLogMarkdown(input);
130
+ await writeFile(activePath, content, 'utf-8');
131
+ await writeRunMeta(activeRunDir, {
132
+ taskId: input.taskId,
133
+ task: input.task,
134
+ model: input.model,
135
+ channel: input.channel,
136
+ cwd: input.cwd,
137
+ result: {
138
+ ok: false,
139
+ status: 'failed',
140
+ failureCategory: 'unknown',
141
+ model: input.model,
142
+ durationMs: 0,
143
+ stdout: '',
144
+ stderr: '',
145
+ },
146
+ }, 'running');
147
+ return { logPath: activePath, taskId: input.taskId };
148
+ }
149
+ /**
150
+ * Finalize an active run log with execution results.
151
+ * Moves successful runs to `.harness/runs/completed/`.
152
+ */
153
+ export async function finalizeCursorRunLog(logPath, input) {
154
+ const harnessDir = await resolveHarnessDir(input.cwd);
155
+ if (!harnessDir)
156
+ return null;
157
+ const existing = await readFile(logPath, 'utf-8').catch(() => '');
158
+ const taskId = existing ? parseCursorRunLog(existing, logPath).taskId : undefined;
159
+ const content = buildRunLogMarkdown({ ...input, taskId });
160
+ await writeFile(logPath, content, 'utf-8');
161
+ const activeRunDir = runDirForLogPath(logPath);
162
+ if (activeRunDir) {
163
+ await writeRunMeta(activeRunDir, { ...input, taskId });
164
+ }
165
+ const targetLifecycle = input.result.ok ? 'completed' : 'failed';
166
+ const targetDir = path.join(harnessDir, 'runs', targetLifecycle);
167
+ await mkdir(targetDir, { recursive: true });
168
+ if (isRunMarkdownPath(logPath)) {
169
+ const completedPath = await resolveUniqueRunPath(targetDir, path.basename(path.dirname(logPath)));
170
+ await moveRunDirectory(path.dirname(logPath), completedPath);
171
+ return { logPath: path.join(completedPath, 'run.md'), completed: input.result.ok };
172
+ }
173
+ if (!input.result.ok) {
174
+ return { logPath, completed: false };
175
+ }
176
+ const completedPath = path.join(targetDir, path.basename(logPath));
177
+ try {
178
+ await rename(logPath, completedPath);
179
+ }
180
+ catch {
181
+ await writeFile(completedPath, content, 'utf-8');
182
+ await unlink(logPath).catch(() => { });
183
+ }
184
+ return { logPath: completedPath, completed: true };
185
+ }
186
+ /**
187
+ * Query Cursor run records from `.harness/runs/` (for `cursor-task` history / stale detection).
188
+ */
189
+ export async function queryCursorRuns(cwd, options = {}) {
190
+ const harnessDir = await resolveHarnessDir(cwd);
191
+ if (!harnessDir)
192
+ return [];
193
+ const runsRoot = path.join(harnessDir, 'runs');
194
+ const activeDir = path.join(runsRoot, 'active');
195
+ const completedDir = path.join(runsRoot, 'completed');
196
+ const failedDir = path.join(runsRoot, 'failed');
197
+ const records = [];
198
+ for (const dir of [activeDir, failedDir, completedDir]) {
199
+ let entries;
200
+ try {
201
+ entries = (await readdir(dir)).sort();
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ for (const entry of entries) {
207
+ const entryPath = path.join(dir, entry);
208
+ const logPath = entry.endsWith('.md') ? entryPath : path.join(entryPath, 'run.md');
209
+ const content = await readFile(logPath, 'utf-8').catch(() => '');
210
+ if (!content)
211
+ continue;
212
+ const parsed = parseCursorRunLog(content, logPath);
213
+ parsed.status = resolveRunStatus(parsed, dir === completedDir, options.inMemoryTaskId, dir === failedDir);
214
+ records.push(parsed);
215
+ if (options.taskId && parsed.taskId === options.taskId) {
216
+ return [parsed];
217
+ }
218
+ }
219
+ }
220
+ if (options.taskId)
221
+ return [];
222
+ const activeRecords = records
223
+ .filter((record) => record.status === 'running' || record.status === 'stale' || record.status === 'failed')
224
+ .sort((a, b) => (b.startTime ?? '').localeCompare(a.startTime ?? ''));
225
+ const completedRecords = records
226
+ .filter((record) => record.status === 'completed')
227
+ .sort((a, b) => (b.startTime ?? '').localeCompare(a.startTime ?? ''))
228
+ .slice(0, options.maxCompleted ?? 10);
229
+ return [...activeRecords, ...completedRecords];
230
+ }
231
+ export function parseCursorRunLog(content, logPath) {
232
+ const readField = (field) => {
233
+ const match = content.match(new RegExp(`\\| ${field} \\| ([^|]+) \\|`));
234
+ return match?.[1]?.trim();
235
+ };
236
+ const resultMatch = content.match(/## 结果摘要\s*\n\s*\n([\s\S]*?)(?:\n\n_\(|$)/);
237
+ const rawSummary = resultMatch?.[1]?.trim() ?? '';
238
+ const truncated = content.includes('_(输出已截断)_');
239
+ return {
240
+ taskId: readField('taskId') ?? parseTaskIdFallback(logPath),
241
+ status: 'running',
242
+ logPath,
243
+ task: extractTaskSection(content),
244
+ model: readField('模型') ?? 'default',
245
+ cwd: readField('cwd') ?? '',
246
+ channel: (readField('通道') ?? 'tool'),
247
+ startTime: readField('日期'),
248
+ durationSec: readField('耗时')?.replace(/s$/, ''),
249
+ tableStatus: readField('状态'),
250
+ failureCategory: readField('失败分类'),
251
+ resultSummary: rawSummary === RUN_LOG_RUNNING_PLACEHOLDER ? undefined : rawSummary,
252
+ resultTruncated: truncated,
253
+ };
254
+ }
255
+ export function resolveRunStatus(record, inCompletedDir, inMemoryTaskId, inFailedDir = false) {
256
+ if (inCompletedDir)
257
+ return 'completed';
258
+ if (inFailedDir)
259
+ return 'failed';
260
+ if (record.resultSummary) {
261
+ if (record.tableStatus === 'completed')
262
+ return 'completed';
263
+ return 'failed';
264
+ }
265
+ if (record.tableStatus && record.tableStatus !== 'running') {
266
+ return 'failed';
267
+ }
268
+ if (inMemoryTaskId && record.taskId === inMemoryTaskId) {
269
+ return 'running';
270
+ }
271
+ return 'stale';
272
+ }
273
+ function extractTaskSection(content) {
274
+ const match = content.match(/## 任务\s*\n\s*\n([\s\S]*?)\n\s*\n## /);
275
+ return match?.[1]?.trim() ?? '';
276
+ }
277
+ async function resolveHarnessDir(cwd) {
278
+ const harnessDir = path.join(cwd, '.harness');
279
+ try {
280
+ await access(harnessDir);
281
+ return harnessDir;
282
+ }
283
+ catch {
284
+ return null;
285
+ }
286
+ }
287
+ /**
288
+ * Derive a filesystem-safe slug from a task description.
289
+ *
290
+ * Rules:
291
+ * - Take first 40 characters
292
+ * - Replace non-alphanumeric (except CJK) with `-`
293
+ * - Collapse consecutive `-`
294
+ * - Trim leading/trailing `-`
295
+ * - Empty task → `untitled`
296
+ */
297
+ export function deriveRunSlug(task) {
298
+ const raw = task.trim().slice(0, 40);
299
+ if (!raw)
300
+ return 'untitled';
301
+ // Allow alphanumeric, CJK unified ideographs (U+4E00–U+9FFF), hiragana, katakana
302
+ // Replace everything else with `-`
303
+ const slug = raw
304
+ .replace(/[^\p{L}\p{N}]/gu, '-')
305
+ .replace(/-+/g, '-')
306
+ .replace(/^-|-$/g, '');
307
+ return slug || 'untitled';
308
+ }
309
+ /**
310
+ * Canonical one-shot run id format.
311
+ *
312
+ * One-shot Cursor/Pi run ids are directory basenames under
313
+ * `.harness/runs/<state>/`. New runs use the form produced by
314
+ * `computeRunDir` / `persistCursorRunLog`:
315
+ *
316
+ * YYYY-MM-DD-<kebab-slug>[-N]
317
+ *
318
+ * where `-N` is the collision suffix appended by `resolveUniqueRunPath`
319
+ * (e.g. `-2`, `-3`). Legacy `.md`-only run logs (read-only compatible) may
320
+ * use the bare `YYYY-MM-DD-<slug>.md` form.
321
+ */
322
+ export const CANONICAL_ONE_SHOT_RUN_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(-[a-z0-9]+)*(?:-(?!r\d)\d+)?$/;
323
+ /** Bare `.md` legacy log name: `YYYY-MM-DD-<slug>.md` (read-only compatibility). */
324
+ const LEGACY_ONE_SHOT_MD_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$/;
325
+ /**
326
+ * Validate a one-shot run id (or legacy `.md` log basename) against the
327
+ * canonical format. Legacy ids remain readable; this only emits warnings.
328
+ *
329
+ * Accepts either a directory-style id (`YYYY-MM-DD-<slug>[-N]`) or a legacy
330
+ * markdown log name (`YYYY-MM-DD-<slug>.md`).
331
+ */
332
+ export function validateOneShotRunId(runId) {
333
+ const warnings = [];
334
+ const trimmed = runId.trim();
335
+ if (!trimmed) {
336
+ warnings.push("one-shot runId cannot be empty");
337
+ return { warnings };
338
+ }
339
+ const stripped = trimmed.endsWith(".md")
340
+ ? trimmed.slice(0, -3)
341
+ : trimmed;
342
+ if (CANONICAL_ONE_SHOT_RUN_ID_PATTERN.test(stripped) ||
343
+ LEGACY_ONE_SHOT_MD_PATTERN.test(trimmed)) {
344
+ return { warnings };
345
+ }
346
+ warnings.push(`one-shot runId "${trimmed}" does not match canonical YYYY-MM-DD-<slug>; legacy run evidence remains readable`);
347
+ return { warnings };
348
+ }
349
+ /**
350
+ * If `baseName` already exists in `dir`, append `-2`, `-3`, etc.
351
+ */
352
+ export async function resolveUniqueRunPath(dir, baseName) {
353
+ let candidate = path.join(dir, baseName);
354
+ let counter = 1;
355
+ while (true) {
356
+ try {
357
+ await access(candidate);
358
+ // File exists — try next suffix
359
+ counter += 1;
360
+ const ext = path.extname(baseName);
361
+ const stem = path.basename(baseName, ext);
362
+ candidate = path.join(dir, `${stem}-${counter}${ext}`);
363
+ }
364
+ catch {
365
+ // File does not exist — use this path
366
+ return candidate;
367
+ }
368
+ }
369
+ }
370
+ // ---------------------------------------------------------------------------
371
+ // Markdown builder
372
+ // ---------------------------------------------------------------------------
373
+ function buildRunLogMarkdown(input) {
374
+ const durationSec = (input.result.durationMs / 1000).toFixed(1);
375
+ const outputText = input.result.stdout || input.result.stderr || '(no output)';
376
+ const truncated = truncateOutput(outputText, RUN_LOG_RESULT_MAX);
377
+ const lines = [
378
+ '# Cursor 执行记录',
379
+ '',
380
+ '| 字段 | 值 |',
381
+ '|------|----|',
382
+ ];
383
+ if (input.taskId) {
384
+ lines.push(`| taskId | ${input.taskId} |`);
385
+ }
386
+ lines.push(`| 日期 | ${new Date().toISOString().slice(0, 10)} |`, `| 模型 | ${input.model} |`, `| 耗时 | ${durationSec}s |`, `| 状态 | ${input.result.ok ? 'completed' : input.result.status} |`, `| 通道 | ${input.channel} |`, `| cwd | ${input.cwd} |`);
387
+ if (!input.result.ok && input.result.failureCategory !== 'success') {
388
+ lines.push(`| 失败分类 | ${input.result.failureCategory} |`);
389
+ }
390
+ lines.push('', '## 任务', '', input.task, '', '## 结果摘要', '', truncated.text);
391
+ if (truncated.truncated) {
392
+ lines.push('', '_(输出已截断)_');
393
+ }
394
+ lines.push('');
395
+ return lines.join('\n');
396
+ }
397
+ function buildActiveRunLogMarkdown(input) {
398
+ const lines = [
399
+ '# Cursor 执行记录',
400
+ '',
401
+ '| 字段 | 值 |',
402
+ '|------|----|',
403
+ `| taskId | ${input.taskId} |`,
404
+ `| 日期 | ${new Date().toISOString().slice(0, 10)} |`,
405
+ `| 模型 | ${input.model} |`,
406
+ `| 耗时 | - |`,
407
+ `| 状态 | running |`,
408
+ `| 通道 | ${input.channel} |`,
409
+ `| cwd | ${input.cwd} |`,
410
+ '',
411
+ '## 任务',
412
+ '',
413
+ input.task,
414
+ '',
415
+ '## 结果摘要',
416
+ '',
417
+ RUN_LOG_RUNNING_PLACEHOLDER,
418
+ '',
419
+ ];
420
+ return lines.join('\n');
421
+ }
@@ -0,0 +1,199 @@
1
+ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getArtifactPath } from '../shared/artifacts-core.js';
4
+ import { getDagRunDir, readDagRunState } from '../workflows/dag/lifecycle.js';
5
+ import { parseCursorRunLog, resolveRunStatus } from './one-shot-runs.js';
6
+ import { getTaskDir } from '../task/runtime.js';
7
+ export async function promoteRunEvidence(repoRoot, taskId, runId) {
8
+ const dag = await loadCompletedDagEvidence(repoRoot, runId).catch(() => null);
9
+ if (dag) {
10
+ return writePromotion(repoRoot, taskId, runId, 'dag', buildDagModifyLog(taskId, runId, dag), buildDagVerifyResult(taskId, runId, dag));
11
+ }
12
+ const oneShot = await loadCompletedOneShotEvidence(repoRoot, runId);
13
+ if (oneShot) {
14
+ return writePromotion(repoRoot, taskId, runId, 'one-shot', buildOneShotModifyLog(taskId, runId, oneShot), buildOneShotVerifyResult(taskId, runId, oneShot));
15
+ }
16
+ throw new Error(`completed run evidence not found for run-id: ${runId}`);
17
+ }
18
+ export async function closeoutTask(repoRoot, taskId) {
19
+ const taskDir = getTaskDir(repoRoot, taskId);
20
+ const modifyPath = getArtifactPath(taskDir, 'implement');
21
+ const verifyPath = getArtifactPath(taskDir, 'verify');
22
+ const modify = await readFile(modifyPath, 'utf-8').catch(() => '');
23
+ const verify = await readFile(verifyPath, 'utf-8').catch(() => '');
24
+ const slug = taskId.replace(/^\d{4}-\d{2}-\d{2}-/, '');
25
+ const datePrefix = taskId.match(/^\d{4}-\d{2}-\d{2}/)?.[0] ?? new Date().toISOString().slice(0, 10);
26
+ const progressDir = path.join(repoRoot, 'docs', 'progress');
27
+ await mkdir(progressDir, { recursive: true });
28
+ const progressPath = path.join(progressDir, `${datePrefix}-${slug}.md`);
29
+ const verificationSummary = summarizeVerification(verify);
30
+ const content = [
31
+ `# ${taskId}`,
32
+ '',
33
+ '## Summary',
34
+ '',
35
+ summarizeSection(modify) || '- No promoted modification summary found.',
36
+ '',
37
+ '## Verification',
38
+ '',
39
+ verificationSummary,
40
+ '',
41
+ '## Remaining Risk',
42
+ '',
43
+ verificationSummary.includes('No concrete verification evidence')
44
+ ? '- Verification evidence is incomplete; do not treat this closeout as proof of passing tests.'
45
+ : '- Review remaining risks in task artifacts and related run facts before archiving.',
46
+ '',
47
+ ].join('\n');
48
+ await writeFile(progressPath, content, 'utf-8');
49
+ return { taskId, progressPath };
50
+ }
51
+ async function writePromotion(repoRoot, taskId, runId, sourceKind, modifyLog, verifyResult) {
52
+ const taskDir = getTaskDir(repoRoot, taskId);
53
+ const modifyLogPath = getArtifactPath(taskDir, 'implement');
54
+ const verifyResultPath = getArtifactPath(taskDir, 'verify');
55
+ await mkdir(path.dirname(modifyLogPath), { recursive: true });
56
+ await writeFile(modifyLogPath, modifyLog, 'utf-8');
57
+ await writeFile(verifyResultPath, verifyResult, 'utf-8');
58
+ return { taskId, runId, sourceKind, modifyLogPath, verifyResultPath };
59
+ }
60
+ async function loadCompletedDagEvidence(repoRoot, runId) {
61
+ const runDir = getDagRunDir(repoRoot, 'completed', runId);
62
+ const state = await readDagRunState(runDir);
63
+ if (state.status !== 'finished') {
64
+ throw new Error(`completed DAG run ${runId} is not finished (status=${state.status})`);
65
+ }
66
+ return state;
67
+ }
68
+ async function loadCompletedOneShotEvidence(repoRoot, runId) {
69
+ const completedDir = path.join(repoRoot, '.harness', 'runs', 'completed');
70
+ const direct = await readOneShotRecord(path.join(completedDir, runId, 'run.md'), true);
71
+ if (direct)
72
+ return direct;
73
+ const legacy = await readOneShotRecord(path.join(completedDir, `${runId}.md`), true);
74
+ if (legacy)
75
+ return legacy;
76
+ const entries = await readdir(completedDir).catch(() => []);
77
+ for (const entry of entries) {
78
+ const logPath = entry.endsWith('.md')
79
+ ? path.join(completedDir, entry)
80
+ : path.join(completedDir, entry, 'run.md');
81
+ const record = await readOneShotRecord(logPath, true);
82
+ if (record && oneShotRunIdFromLogPath(logPath) === runId)
83
+ return record;
84
+ }
85
+ return null;
86
+ }
87
+ async function readOneShotRecord(logPath, completed) {
88
+ const content = await readFile(logPath, 'utf-8').catch(() => '');
89
+ if (!content)
90
+ return null;
91
+ const record = parseCursorRunLog(content, logPath);
92
+ record.status = resolveRunStatus(record, completed);
93
+ return record.status === 'completed' ? record : null;
94
+ }
95
+ function oneShotRunIdFromLogPath(logPath) {
96
+ if (path.basename(logPath) === 'run.md')
97
+ return path.basename(path.dirname(logPath));
98
+ return path.basename(logPath, '.md');
99
+ }
100
+ function buildDagModifyLog(taskId, runId, state) {
101
+ const finished = Object.values(state.nodes).filter((node) => node.status === 'FINISHED');
102
+ return [
103
+ '# 修改记录',
104
+ '',
105
+ '## 1. 修改文件清单',
106
+ '',
107
+ `- Promoted from completed DAG run \`${runId}\` for task \`${taskId}\`.`,
108
+ '',
109
+ '## 2. 关键改动说明',
110
+ '',
111
+ ...finished.map((node) => `- ${node.id}: ${excerpt(node.assistantText ?? node.stdout ?? 'finished')}`),
112
+ '',
113
+ ].join('\n');
114
+ }
115
+ function buildDagVerifyResult(taskId, runId, state) {
116
+ const nodes = Object.values(state.nodes);
117
+ const verifyNodes = nodes.filter((node) => /verify|ci|check/i.test(node.id));
118
+ const failed = nodes.filter((node) => node.status === 'ERROR');
119
+ const lines = [
120
+ '# 验证结果',
121
+ '',
122
+ '## 1. 执行的命令',
123
+ '',
124
+ verifyNodes.length > 0
125
+ ? verifyNodes.map((node) => `- DAG node \`${node.id}\`: ${node.failureCategory ?? node.status}`).join('\n')
126
+ : '- No dedicated verify/ci/check DAG node was found in completed run facts.',
127
+ '',
128
+ '## 2. 通过项',
129
+ '',
130
+ `- Promoted deterministic evidence from completed DAG run \`${runId}\` for task \`${taskId}\`.`,
131
+ ...verifyNodes.filter((node) => node.status === 'FINISHED').map((node) => `- ${node.id}: ${node.failureCategory ?? 'success'}`),
132
+ '',
133
+ '## 3. 失败项',
134
+ '',
135
+ failed.length > 0
136
+ ? failed.map((node) => `- ${node.id}: ${node.failureCategory ?? 'error'}`).join('\n')
137
+ : '- No failed DAG nodes recorded.',
138
+ '',
139
+ ];
140
+ return lines.join('\n');
141
+ }
142
+ function buildOneShotModifyLog(taskId, runId, record) {
143
+ return [
144
+ '# 修改记录',
145
+ '',
146
+ '## 1. 修改文件清单',
147
+ '',
148
+ `- Promoted from completed one-shot run \`${runId}\` for task \`${taskId}\`.`,
149
+ '',
150
+ '## 2. 关键改动说明',
151
+ '',
152
+ `- Task: ${record.task || '(not recorded)'}`,
153
+ `- Result: ${excerpt(record.resultSummary ?? 'completed')}`,
154
+ '',
155
+ ].join('\n');
156
+ }
157
+ function buildOneShotVerifyResult(taskId, runId, record) {
158
+ return [
159
+ '# 验证结果',
160
+ '',
161
+ '## 1. 执行的命令',
162
+ '',
163
+ '- One-shot run evidence does not expose deterministic shell commands; inspect the run log for details.',
164
+ '',
165
+ '## 2. 通过项',
166
+ '',
167
+ `- One-shot run \`${runId}\` for task \`${taskId}\` completed with status ${record.tableStatus ?? record.status}.`,
168
+ '',
169
+ '## 3. 失败项',
170
+ '',
171
+ record.failureCategory && record.failureCategory !== 'success'
172
+ ? `- Failure category recorded: ${record.failureCategory}`
173
+ : '- No failure category recorded.',
174
+ '',
175
+ ].join('\n');
176
+ }
177
+ function summarizeSection(content) {
178
+ return content
179
+ .split(/\r?\n/)
180
+ .filter((line) => line.trim().startsWith('- '))
181
+ .slice(0, 8)
182
+ .join('\n');
183
+ }
184
+ function summarizeVerification(content) {
185
+ if (!/npm|vitest|check-repo|typecheck|lint|CI|exit code|passed|failed/i.test(content)) {
186
+ return '- No concrete verification evidence found in task artifacts.';
187
+ }
188
+ const bullets = summarizeSection(content);
189
+ return bullets || '- Verification artifact exists; inspect task artifacts for details.';
190
+ }
191
+ function excerpt(value, max = 220) {
192
+ const normalized = value.replace(/\s+/g, ' ').trim();
193
+ if (normalized.length <= max)
194
+ return normalized;
195
+ return `${normalized.slice(0, max)}...`;
196
+ }
197
+ export async function pathExists(filePath) {
198
+ return Boolean(await stat(filePath).catch(() => null));
199
+ }