@tea-agent/loop-agent 0.1.0 → 0.2.1

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 (143) hide show
  1. package/AGENTS.md +62 -45
  2. package/CHANGELOG.md +60 -28
  3. package/README.md +160 -124
  4. package/bin/loop-agent.js +21 -21
  5. package/dist/adapters/index.js +3 -2
  6. package/dist/adapters/loop-agent.js +44 -2
  7. package/dist/application/dag/args.js +420 -0
  8. package/dist/application/dag/generate-task-dag.js +280 -0
  9. package/dist/application/dag/report-dag.js +14 -0
  10. package/dist/application/dag/run-dag.js +106 -0
  11. package/dist/application/dag/validate-dag.js +102 -0
  12. package/dist/application/loop/run-action.js +23 -0
  13. package/dist/cli/catalog.js +2 -237
  14. package/dist/cli/command-definitions.js +571 -0
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/program.js +65 -1
  17. package/dist/cli/router.js +13 -0
  18. package/dist/cli-governance/active-residue-check.js +38 -0
  19. package/dist/commands/dag-report.js +6 -107
  20. package/dist/commands/dag-run-task.js +8 -466
  21. package/dist/commands/dag-validate.js +7 -179
  22. package/dist/commands/examples.js +90 -0
  23. package/dist/commands/init.js +1518 -0
  24. package/dist/commands/loop.js +57 -31
  25. package/dist/commands/pi-prompt.js +2 -9
  26. package/dist/commands/run-dag.js +7 -180
  27. package/dist/executors/cursor-executor-artifacts.js +3 -4
  28. package/dist/executors/cursor-worker-client.js +13 -3
  29. package/dist/executors/dag-cursor-executor.js +2 -3
  30. package/dist/executors/dag-pi-executor.js +3 -4
  31. package/dist/executors/dag-static-executor.js +2 -5
  32. package/dist/executors/pi-defaults.js +9 -0
  33. package/dist/executors/shell-executor.js +12 -20
  34. package/dist/governance/manifest-types.js +1 -0
  35. package/dist/infrastructure/harness/active-residue-policy.js +73 -0
  36. package/dist/infrastructure/harness/artifact-store.js +72 -0
  37. package/dist/infrastructure/harness/atomic-write.js +49 -0
  38. package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
  39. package/dist/infrastructure/harness/loop-action-store.js +23 -0
  40. package/dist/infrastructure/harness/loop-store.js +41 -0
  41. package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
  42. package/dist/infrastructure/harness/task-store.js +77 -0
  43. package/dist/records/one-shot-runs.js +26 -61
  44. package/dist/records/promotion.js +3 -4
  45. package/dist/shared/artifacts-core.js +5 -5
  46. package/dist/shared/logger.js +9 -15
  47. package/dist/task/delegate.js +4 -4
  48. package/dist/task/runtime.js +5 -7
  49. package/dist/task/state.js +6 -20
  50. package/dist/workflows/dag/convergence/controller.js +277 -0
  51. package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
  52. package/dist/workflows/dag/dynamic-runtime/loop-until.js +156 -0
  53. package/dist/workflows/dag/dynamic-runtime/map.js +185 -0
  54. package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
  55. package/dist/workflows/dag/dynamic-runtime/shared.js +133 -0
  56. package/dist/workflows/dag/failure-routing.js +82 -0
  57. package/dist/workflows/dag/lifecycle.js +101 -8
  58. package/dist/workflows/dag/node-execution.js +262 -0
  59. package/dist/workflows/dag/report.js +73 -1
  60. package/dist/workflows/dag/run-store.js +36 -0
  61. package/dist/workflows/dag/runner.js +82 -1341
  62. package/dist/workflows/dag/scheduler.js +84 -0
  63. package/dist/workflows/dag/upstream-artifacts.js +20 -18
  64. package/dist/workflows/loop/actions/cursor-fix.js +191 -0
  65. package/dist/workflows/loop/actions/dag-action.js +130 -0
  66. package/dist/workflows/loop/actions/pi-review.js +267 -0
  67. package/dist/workflows/loop/actions/shared.js +157 -0
  68. package/dist/workflows/loop/actions/shell-verify.js +82 -0
  69. package/dist/workflows/loop/actions/types.js +1 -0
  70. package/dist/workflows/loop/actions/workflow-action.js +255 -0
  71. package/dist/workflows/loop/actions.js +55 -1212
  72. package/dist/workflows/loop/closeout.js +5 -4
  73. package/dist/workflows/loop/context.js +2 -3
  74. package/dist/workflows/loop/events.js +3 -2
  75. package/dist/workflows/loop/policy/auto-policy.js +104 -0
  76. package/dist/workflows/loop/policy/cursor-fix-policy.js +31 -0
  77. package/dist/workflows/loop/rounds.js +3 -3
  78. package/dist/workflows/loop/signals.js +4 -7
  79. package/dist/workflows/loop/state.js +11 -11
  80. package/docs/README.md +47 -44
  81. package/docs/agent-dag-recovery-playbook.md +32 -6
  82. package/docs/agent-dag-runner.md +17 -17
  83. package/docs/architecture/runtime-boundaries.md +147 -0
  84. package/docs/cursor-executor-usage.md +5 -5
  85. package/docs/decisions/README.md +2 -2
  86. package/docs/design/README.md +24 -24
  87. package/docs/development-principles.md +50 -50
  88. package/docs/dynamic-workflow-dag-engine-roadmap.md +6 -6
  89. package/docs/exec-plans/README.md +4 -4
  90. package/docs/exec-plans/active/README.md +10 -5
  91. package/docs/exec-plans/completed/README.md +9 -5
  92. package/docs/feature-workflow.md +111 -109
  93. package/docs/harness-methodology-verification.md +18 -18
  94. package/docs/loop-agent-harness.md +36 -36
  95. package/docs/production-readiness.md +96 -0
  96. package/docs/progress/README.md +2 -2
  97. package/docs/reports/README.md +4 -2
  98. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
  99. package/docs/templates/agent-dag-process-supervisor.prompt.md +2 -2
  100. package/docs/templates/agent-dag-report.schema.json +33 -2
  101. package/docs/templates/agent-dag-review-verdict.prompt.md +1 -1
  102. package/docs/templates/agent-dag.base.json +195 -195
  103. package/docs/templates/agent-dag.final-verification.json +190 -190
  104. package/docs/templates/agent-dag.schema.json +17 -17
  105. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  106. package/docs/templates/hybrid-dag.json +193 -193
  107. package/docs/templates/production-readiness-checklist.md +57 -0
  108. package/docs/templates/progress-log.md +7 -7
  109. package/docs/templates/project-start-checklist.md +8 -8
  110. package/docs/templates/qa-report.md +17 -11
  111. package/docs/templates/sprint-contract.md +19 -19
  112. package/docs/verification-matrix.md +37 -26
  113. package/examples/example-dag.json +51 -51
  114. package/examples/hybrid-loop-agent-dag.json +194 -194
  115. package/harness.json +5 -5
  116. package/package.json +62 -61
  117. package/skills/ai-engineering-context/SKILL.md +21 -21
  118. package/skills/loop-agent/SKILL.md +56 -171
  119. package/skills/loop-agent/references/README.md +6 -2
  120. package/skills/loop-agent/references/command-reference.md +107 -65
  121. package/skills/loop-agent/references/harness-policy.md +115 -115
  122. package/skills/loop-agent/references/hybrid-dag.md +30 -30
  123. package/skills/loop-agent/references/learned/README.md +13 -13
  124. package/skills/loop-agent/references/long-running-loop.md +59 -0
  125. package/skills/loop-agent/references/model-routing.md +1 -1
  126. package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
  127. package/skills/loop-agent/references/pi-prompt.md +9 -9
  128. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +0 -2
  129. package/skills/loop-agent/references/post-implementation-and-patterns.md +7 -7
  130. package/skills/loop-agent/references/task-workflow.md +19 -19
  131. package/skills/loop-agent/references/verification-and-failure-handling.md +54 -0
  132. package/skills/requesting-code-review/SKILL.md +40 -40
  133. package/skills/requesting-code-review/code-reviewer.md +4 -4
  134. package/skills/systematic-debugging/CREATION-LOG.md +43 -43
  135. package/skills/systematic-debugging/SKILL.md +113 -113
  136. package/skills/systematic-debugging/condition-based-waiting.md +20 -20
  137. package/skills/systematic-debugging/defense-in-depth.md +27 -27
  138. package/skills/systematic-debugging/root-cause-tracing.md +38 -38
  139. package/skills/systematic-debugging/test-academic.md +6 -6
  140. package/skills/systematic-debugging/test-pressure-1.md +6 -6
  141. package/skills/systematic-debugging/test-pressure-2.md +2 -2
  142. package/skills/systematic-debugging/test-pressure-3.md +6 -6
  143. package/skills/verification-before-completion/SKILL.md +37 -37
@@ -0,0 +1,72 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { writeJsonAtomic, writeTextAtomic, } from "./atomic-write.js";
4
+ function resolveRepoRootFromDagRunDir(runDir) {
5
+ const normalized = path.resolve(runDir);
6
+ const lifecycleDir = path.dirname(normalized);
7
+ const dagRunsDir = path.dirname(lifecycleDir);
8
+ const harnessDir = path.dirname(dagRunsDir);
9
+ const repoRoot = path.dirname(harnessDir);
10
+ if (path.basename(harnessDir) !== ".harness" ||
11
+ path.basename(dagRunsDir) !== "dag-runs") {
12
+ return undefined;
13
+ }
14
+ return repoRoot;
15
+ }
16
+ function resolveRepoRootFromHarnessPath(targetPath) {
17
+ const normalized = path.resolve(targetPath);
18
+ const parts = normalized.split(path.sep);
19
+ const harnessIndex = parts.indexOf(".harness");
20
+ if (harnessIndex <= 0) {
21
+ return undefined;
22
+ }
23
+ return parts.slice(0, harnessIndex).join(path.sep);
24
+ }
25
+ function dagWriteOptions(runDir, options) {
26
+ return {
27
+ repoRoot: options?.repoRoot ?? resolveRepoRootFromDagRunDir(runDir),
28
+ allowCompletedFactsWrite: options?.allowCompletedFactsWrite,
29
+ };
30
+ }
31
+ function pathWriteOptions(targetPath, options) {
32
+ return {
33
+ repoRoot: options?.repoRoot ?? resolveRepoRootFromHarnessPath(targetPath),
34
+ allowCompletedFactsWrite: options?.allowCompletedFactsWrite,
35
+ };
36
+ }
37
+ function completedRelocationWriteOptions(runDir) {
38
+ return {
39
+ ...dagWriteOptions(runDir),
40
+ allowCompletedFactsWrite: true,
41
+ };
42
+ }
43
+ export function dagCursorSdkArtifactDir(runDir, nodeId) {
44
+ return path.join(runDir, "artifacts", nodeId);
45
+ }
46
+ export async function writeDagNodeTextArtifact(runDir, nodeId, fileName, content, options) {
47
+ const targetPath = path.join(runDir, nodeId, fileName);
48
+ await writeTextAtomic(targetPath, content, dagWriteOptions(runDir, options));
49
+ return targetPath;
50
+ }
51
+ export async function writeDagNodeJsonArtifact(runDir, nodeId, fileName, value, options) {
52
+ const targetPath = path.join(runDir, nodeId, fileName);
53
+ await writeJsonAtomic(targetPath, value, dagWriteOptions(runDir, options));
54
+ return targetPath;
55
+ }
56
+ export async function writeDagRunJsonArtifact(runDir, relativePath, value, options) {
57
+ const targetPath = path.join(runDir, relativePath);
58
+ await writeJsonAtomic(targetPath, value, dagWriteOptions(runDir, options));
59
+ return targetPath;
60
+ }
61
+ export async function ensureDagNodeArtifactDir(runDir, nodeId) {
62
+ const artifactDir = dagCursorSdkArtifactDir(runDir, nodeId);
63
+ await mkdir(artifactDir, { recursive: true });
64
+ return artifactDir;
65
+ }
66
+ export async function writeTextArtifactFile(targetPath, content, options) {
67
+ await writeTextAtomic(targetPath, content, pathWriteOptions(targetPath, options));
68
+ }
69
+ /** Rewrite node records after active/paused -> completed run-dir transfer. */
70
+ export async function writeRelocatedDagNodeRecord(runDir, nodeId, record) {
71
+ await writeDagRunJsonArtifact(runDir, `${nodeId}.json`, record, completedRelocationWriteOptions(runDir));
72
+ }
@@ -0,0 +1,49 @@
1
+ import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { assertHarnessWriteAllowed, } from "./completed-facts-guard.js";
4
+ async function writeAtomic(targetPath, content, options) {
5
+ assertHarnessWriteAllowed(targetPath, options);
6
+ const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
7
+ ? path.resolve(options.repoRoot, targetPath)
8
+ : path.resolve(targetPath);
9
+ await mkdir(path.dirname(resolved), { recursive: true });
10
+ const tempPath = `${resolved}.${process.pid}.tmp`;
11
+ try {
12
+ await writeFile(tempPath, content, "utf-8");
13
+ try {
14
+ await rename(tempPath, resolved);
15
+ }
16
+ catch {
17
+ await copyFile(tempPath, resolved);
18
+ await unlink(tempPath).catch(() => { });
19
+ }
20
+ }
21
+ catch (error) {
22
+ await unlink(tempPath).catch(() => { });
23
+ throw error;
24
+ }
25
+ }
26
+ export async function writeJsonAtomic(targetPath, value, options) {
27
+ const json = `${JSON.stringify(value, null, 2)}\n`;
28
+ await writeAtomic(targetPath, json, options);
29
+ }
30
+ export async function writeTextAtomic(targetPath, text, options) {
31
+ await writeAtomic(targetPath, text, options);
32
+ }
33
+ export function serializeJsonl(entries) {
34
+ if (entries.length === 0)
35
+ return "";
36
+ return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
37
+ }
38
+ export async function writeJsonlAtomic(targetPath, entries, options) {
39
+ await writeAtomic(targetPath, serializeJsonl(entries), options);
40
+ }
41
+ export async function appendJsonlLineAtomic(targetPath, entry, options) {
42
+ assertHarnessWriteAllowed(targetPath, options);
43
+ const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
44
+ ? path.resolve(options.repoRoot, targetPath)
45
+ : path.resolve(targetPath);
46
+ const existing = await readFile(resolved, "utf-8").catch(() => "");
47
+ const prefix = existing.length === 0 || existing.endsWith("\n") ? existing : `${existing}\n`;
48
+ await writeAtomic(targetPath, `${prefix}${JSON.stringify(entry)}\n`, options);
49
+ }
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ const COMPLETED_DAG_RUNS_PREFIX = ".harness/dag-runs/completed/";
3
+ const COMPLETED_RUNS_PREFIX = ".harness/runs/completed/";
4
+ /** Normalize a target path to a repo-relative POSIX path for guard checks. */
5
+ export function normalizeHarnessRelativePath(targetPath, repoRoot) {
6
+ const posixInput = targetPath.split(path.sep).join("/");
7
+ if (!repoRoot) {
8
+ const normalized = posixInput.replace(/^\.\/+/, "");
9
+ const harnessIndex = normalized.indexOf("/.harness/");
10
+ if (harnessIndex >= 0) {
11
+ return normalized.slice(harnessIndex + 1);
12
+ }
13
+ return normalized;
14
+ }
15
+ const resolvedRoot = path.resolve(repoRoot);
16
+ const resolvedTarget = path.isAbsolute(targetPath)
17
+ ? path.resolve(targetPath)
18
+ : path.resolve(resolvedRoot, targetPath);
19
+ const relative = path.relative(resolvedRoot, resolvedTarget);
20
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
21
+ return posixInput.replace(/^\.\/+/, "");
22
+ }
23
+ return relative.split(path.sep).join("/");
24
+ }
25
+ function isCompletedFactsPath(relativePath) {
26
+ return (relativePath.startsWith(COMPLETED_DAG_RUNS_PREFIX) ||
27
+ relativePath === ".harness/dag-runs/completed" ||
28
+ relativePath.startsWith(COMPLETED_RUNS_PREFIX) ||
29
+ relativePath === ".harness/runs/completed");
30
+ }
31
+ /** Throw when a write would mutate completed DAG or one-shot run facts. */
32
+ export function assertHarnessWriteAllowed(targetPath, options) {
33
+ if (options?.allowCompletedFactsWrite) {
34
+ return;
35
+ }
36
+ const relative = normalizeHarnessRelativePath(targetPath, options?.repoRoot);
37
+ if (isCompletedFactsPath(relative)) {
38
+ throw new Error(`harness write guard: refusing to mutate completed run facts: ${relative}`);
39
+ }
40
+ }
@@ -0,0 +1,23 @@
1
+ import path from "node:path";
2
+ import { writeJsonAtomic, writeTextAtomic, } from "./atomic-write.js";
3
+ function loopActionFile(repoRoot, taskId, subdir, filename) {
4
+ return path.join(repoRoot, ".harness", "tasks", taskId, "loop", subdir, filename);
5
+ }
6
+ function loopActionWriteOptions(repoRoot) {
7
+ return { repoRoot };
8
+ }
9
+ export async function writeLoopShellVerifyRecord(repoRoot, taskId, round, record) {
10
+ await writeJsonAtomic(loopActionFile(repoRoot, taskId, "verification", `round-${round}.json`), record, loopActionWriteOptions(repoRoot));
11
+ }
12
+ export async function writeLoopCursorFixRecord(repoRoot, taskId, round, record) {
13
+ await writeJsonAtomic(loopActionFile(repoRoot, taskId, "reviews", `round-${round}-cursor-fix.json`), record, loopActionWriteOptions(repoRoot));
14
+ }
15
+ export async function writeLoopDagRecord(repoRoot, taskId, round, record) {
16
+ await writeJsonAtomic(loopActionFile(repoRoot, taskId, "dag", `round-${round}.json`), record, loopActionWriteOptions(repoRoot));
17
+ }
18
+ export async function writeLoopPiReviewRecord(repoRoot, taskId, round, record) {
19
+ await writeJsonAtomic(loopActionFile(repoRoot, taskId, "reviews", `round-${round}.json`), record, loopActionWriteOptions(repoRoot));
20
+ }
21
+ export async function writeLoopPiReviewPrompt(repoRoot, taskId, round, prompt) {
22
+ await writeTextAtomic(loopActionFile(repoRoot, taskId, "reviews", `round-${round}-prompt.md`), prompt, loopActionWriteOptions(repoRoot));
23
+ }
@@ -0,0 +1,41 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { appendJsonlLineAtomic, writeJsonAtomic, writeJsonlAtomic, writeTextAtomic, } from "./atomic-write.js";
4
+ function loopDir(repoRoot, taskId) {
5
+ return path.join(repoRoot, ".harness", "tasks", taskId, "loop");
6
+ }
7
+ function loopFile(repoRoot, taskId, filename) {
8
+ return path.join(loopDir(repoRoot, taskId), filename);
9
+ }
10
+ function loopWriteOptions(repoRoot) {
11
+ return { repoRoot };
12
+ }
13
+ export async function ensureLoopDir(repoRoot, taskId) {
14
+ const dir = loopDir(repoRoot, taskId);
15
+ await mkdir(dir, { recursive: true });
16
+ return dir;
17
+ }
18
+ export async function writeLoopState(repoRoot, taskId, state) {
19
+ await writeJsonAtomic(loopFile(repoRoot, taskId, "state.json"), state, loopWriteOptions(repoRoot));
20
+ }
21
+ export async function writeLoopObjective(repoRoot, taskId, text) {
22
+ await writeTextAtomic(loopFile(repoRoot, taskId, "objective.md"), text, loopWriteOptions(repoRoot));
23
+ }
24
+ export async function writeLoopContext(repoRoot, taskId, text) {
25
+ await writeTextAtomic(loopFile(repoRoot, taskId, "context.md"), text, loopWriteOptions(repoRoot));
26
+ }
27
+ export async function writeLoopCloseout(repoRoot, taskId, text) {
28
+ await writeTextAtomic(loopFile(repoRoot, taskId, "closeout.md"), text, loopWriteOptions(repoRoot));
29
+ }
30
+ export async function writeLoopRounds(repoRoot, taskId, entries) {
31
+ await writeJsonlAtomic(loopFile(repoRoot, taskId, "rounds.jsonl"), entries, loopWriteOptions(repoRoot));
32
+ }
33
+ export async function writeLoopSignals(repoRoot, taskId, entries) {
34
+ await writeJsonlAtomic(loopFile(repoRoot, taskId, "signals.jsonl"), entries, loopWriteOptions(repoRoot));
35
+ }
36
+ export async function writeLoopEvents(repoRoot, taskId, entries) {
37
+ await writeJsonlAtomic(loopFile(repoRoot, taskId, "events.jsonl"), entries, loopWriteOptions(repoRoot));
38
+ }
39
+ export async function appendLoopEventLine(repoRoot, taskId, entry) {
40
+ await appendJsonlLineAtomic(loopFile(repoRoot, taskId, "events.jsonl"), entry, loopWriteOptions(repoRoot));
41
+ }
@@ -0,0 +1,94 @@
1
+ import { access, cp, mkdir, rename, rm, unlink } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { writeJsonAtomic, writeTextAtomic } from "./atomic-write.js";
4
+ import { assertHarnessWriteAllowed } from "./completed-facts-guard.js";
5
+ function runsDir(repoRoot, lifecycle) {
6
+ return path.join(repoRoot, ".harness", "runs", lifecycle);
7
+ }
8
+ function activeWriteOptions(repoRoot) {
9
+ return { repoRoot };
10
+ }
11
+ function completedTransferWriteOptions(repoRoot) {
12
+ return { repoRoot, allowCompletedFactsWrite: true };
13
+ }
14
+ export async function ensureOneShotRunDirs(repoRoot) {
15
+ for (const lifecycle of ["active", "completed", "failed"]) {
16
+ await mkdir(runsDir(repoRoot, lifecycle), { recursive: true });
17
+ }
18
+ }
19
+ /**
20
+ * If `baseName` already exists in the lifecycle dir, append `-2`, `-3`, etc.
21
+ */
22
+ export async function resolveUniqueOneShotRunPath(repoRoot, lifecycle, baseName) {
23
+ const dir = runsDir(repoRoot, lifecycle);
24
+ let candidate = path.join(dir, baseName);
25
+ let counter = 1;
26
+ while (true) {
27
+ try {
28
+ await access(candidate);
29
+ counter += 1;
30
+ const ext = path.extname(baseName);
31
+ const stem = path.basename(baseName, ext);
32
+ candidate = path.join(dir, `${stem}-${counter}${ext}`);
33
+ }
34
+ catch {
35
+ return candidate;
36
+ }
37
+ }
38
+ }
39
+ export async function createOneShotActiveRunDir(repoRoot, baseName) {
40
+ await ensureOneShotRunDirs(repoRoot);
41
+ const runDir = await resolveUniqueOneShotRunPath(repoRoot, "active", baseName);
42
+ await mkdir(runDir, { recursive: true });
43
+ return runDir;
44
+ }
45
+ export async function writeOneShotRunMarkdown(repoRoot, runDir, content) {
46
+ await writeTextAtomic(path.join(runDir, "run.md"), content, activeWriteOptions(repoRoot));
47
+ }
48
+ export async function writeOneShotActiveLogAtPath(repoRoot, logPath, content) {
49
+ await writeTextAtomic(logPath, content, activeWriteOptions(repoRoot));
50
+ }
51
+ export async function writeOneShotRunMeta(repoRoot, runDir, meta) {
52
+ await writeJsonAtomic(path.join(runDir, "meta.json"), meta, activeWriteOptions(repoRoot));
53
+ }
54
+ async function moveRunDirectory(fromDir, toDir) {
55
+ await mkdir(path.dirname(toDir), { recursive: true });
56
+ try {
57
+ await rename(fromDir, toDir);
58
+ }
59
+ catch {
60
+ await cp(fromDir, toDir, { recursive: true });
61
+ await rm(fromDir, { recursive: true, force: true });
62
+ }
63
+ }
64
+ export async function transferOneShotRunToCompleted(repoRoot, activeRunDir, destBaseName) {
65
+ await ensureOneShotRunDirs(repoRoot);
66
+ const completedPath = await resolveUniqueOneShotRunPath(repoRoot, "completed", destBaseName);
67
+ assertHarnessWriteAllowed(completedPath, completedTransferWriteOptions(repoRoot));
68
+ await moveRunDirectory(activeRunDir, completedPath);
69
+ return completedPath;
70
+ }
71
+ export async function transferOneShotRunToFailed(repoRoot, activeRunDir, destBaseName) {
72
+ await ensureOneShotRunDirs(repoRoot);
73
+ const failedPath = await resolveUniqueOneShotRunPath(repoRoot, "failed", destBaseName);
74
+ assertHarnessWriteAllowed(failedPath, activeWriteOptions(repoRoot));
75
+ await moveRunDirectory(activeRunDir, failedPath);
76
+ return failedPath;
77
+ }
78
+ /**
79
+ * Legacy `.md`-only finalize path: move or rewrite a flat log into completed/.
80
+ * Completed writes use the guarded transfer bypass inside this store.
81
+ */
82
+ export async function finalizeLegacyRunLogToCompleted(repoRoot, logPath, content) {
83
+ await ensureOneShotRunDirs(repoRoot);
84
+ const completedDir = runsDir(repoRoot, "completed");
85
+ const completedPath = path.join(completedDir, path.basename(logPath));
86
+ try {
87
+ await rename(logPath, completedPath);
88
+ }
89
+ catch {
90
+ await writeTextAtomic(completedPath, content, completedTransferWriteOptions(repoRoot));
91
+ await unlink(logPath).catch(() => { });
92
+ }
93
+ return completedPath;
94
+ }
@@ -0,0 +1,77 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { appendJsonlLineAtomic, writeJsonAtomic, writeTextAtomic, } from "./atomic-write.js";
4
+ function taskDir(repoRoot, taskId) {
5
+ return path.join(repoRoot, ".harness", "tasks", taskId);
6
+ }
7
+ function taskFile(repoRoot, taskId, filename) {
8
+ return path.join(taskDir(repoRoot, taskId), filename);
9
+ }
10
+ function taskWriteOptions(repoRoot) {
11
+ return { repoRoot };
12
+ }
13
+ export function resolveTaskContextFromDir(taskDir) {
14
+ const normalized = path.resolve(taskDir);
15
+ const taskId = path.basename(normalized);
16
+ const tasksDir = path.dirname(normalized);
17
+ const harnessDir = path.dirname(tasksDir);
18
+ const repoRoot = path.dirname(harnessDir);
19
+ if (path.basename(tasksDir) !== "tasks" || path.basename(harnessDir) !== ".harness") {
20
+ throw new Error(`invalid task dir: ${taskDir}`);
21
+ }
22
+ return { repoRoot, taskId };
23
+ }
24
+ export function resolveTaskStateContext(statePath) {
25
+ const normalized = path.resolve(statePath);
26
+ const filename = path.basename(normalized);
27
+ if (filename !== ".workflow_state.json") {
28
+ throw new Error(`invalid workflow state path: ${statePath}`);
29
+ }
30
+ const taskId = path.basename(path.dirname(normalized));
31
+ const tasksDir = path.dirname(path.dirname(normalized));
32
+ const harnessDir = path.dirname(tasksDir);
33
+ const repoRoot = path.dirname(harnessDir);
34
+ if (path.basename(tasksDir) !== "tasks" || path.basename(harnessDir) !== ".harness") {
35
+ throw new Error(`invalid workflow state path: ${statePath}`);
36
+ }
37
+ return { repoRoot, taskId };
38
+ }
39
+ export async function writeTaskConfig(repoRoot, taskId, config) {
40
+ await writeJsonAtomic(taskFile(repoRoot, taskId, "task.json"), config, taskWriteOptions(repoRoot));
41
+ }
42
+ export async function writeWorkflowState(repoRoot, taskId, state) {
43
+ const options = taskWriteOptions(repoRoot);
44
+ const stateFile = taskFile(repoRoot, taskId, ".workflow_state.json");
45
+ const bakFile = `${stateFile}.bak`;
46
+ await writeJsonAtomic(stateFile, state, options);
47
+ await writeJsonAtomic(bakFile, state, options);
48
+ }
49
+ export async function initializeTaskLogStubs(repoRoot, taskId) {
50
+ const options = taskWriteOptions(repoRoot);
51
+ const logsDir = path.join(taskDir(repoRoot, taskId), "logs");
52
+ await writeTextAtomic(path.join(logsDir, "executor.jsonl"), "", options);
53
+ await writeTextAtomic(path.join(logsDir, "decisions.jsonl"), "", options);
54
+ }
55
+ function taskArtifactFile(repoRoot, taskId, fileName) {
56
+ return path.join(taskDir(repoRoot, taskId), "artifacts", fileName);
57
+ }
58
+ function taskLogFile(repoRoot, taskId, fileName) {
59
+ return path.join(taskDir(repoRoot, taskId), "logs", fileName);
60
+ }
61
+ export async function writeTaskArtifactFile(repoRoot, taskId, fileName, content) {
62
+ await writeTextAtomic(taskArtifactFile(repoRoot, taskId, fileName), content, taskWriteOptions(repoRoot));
63
+ }
64
+ export async function appendTaskWorkflowLog(repoRoot, taskId, message) {
65
+ const options = taskWriteOptions(repoRoot);
66
+ const logPath = taskLogFile(repoRoot, taskId, "workflow.log");
67
+ const resolved = options.repoRoot && !path.isAbsolute(logPath)
68
+ ? path.resolve(options.repoRoot, logPath)
69
+ : path.resolve(logPath);
70
+ const existing = await readFile(resolved, "utf-8").catch(() => "");
71
+ const line = `[${new Date().toISOString()}] ${message}\n`;
72
+ const prefix = existing.length === 0 || existing.endsWith("\n") ? existing : `${existing}\n`;
73
+ await writeTextAtomic(logPath, `${prefix}${line}`, options);
74
+ }
75
+ export async function appendTaskLogJsonlRecord(repoRoot, taskId, fileName, record) {
76
+ await appendJsonlLineAtomic(taskLogFile(repoRoot, taskId, fileName), record, taskWriteOptions(repoRoot));
77
+ }
@@ -1,6 +1,7 @@
1
- import { access, cp, mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises';
1
+ import { access, readdir, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { truncateOutput } from '../executors/cursor-executor.js';
4
+ import { createOneShotActiveRunDir, ensureOneShotRunDirs, finalizeLegacyRunLogToCompleted, transferOneShotRunToCompleted, transferOneShotRunToFailed, writeOneShotActiveLogAtPath, writeOneShotRunMarkdown, writeOneShotRunMeta, } from '../infrastructure/harness/one-shot-run-store.js';
4
5
  /** Maximum chars for result summary in the run log. */
5
6
  export const RUN_LOG_RESULT_MAX = 4_000;
6
7
  /** Placeholder written to active logs before execution finishes. */
@@ -19,11 +20,7 @@ export async function computeRunDir(cwd, task) {
19
20
  }
20
21
  const datePrefix = new Date().toISOString().slice(0, 10);
21
22
  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;
23
+ return createOneShotActiveRunDir(cwd, `${datePrefix}-${slug}`);
27
24
  }
28
25
  /**
29
26
  * Persist a human-readable run log under `.harness/runs/`.
@@ -46,44 +43,25 @@ export async function persistCursorRunLog(cwd, input) {
46
43
  const datePrefix = new Date().toISOString().slice(0, 10);
47
44
  const slug = deriveRunSlug(input.task);
48
45
  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 });
46
+ await ensureOneShotRunDirs(cwd);
47
+ const activeRunDir = input.runDir ?? await createOneShotActiveRunDir(cwd, baseName);
57
48
  const content = buildRunLogMarkdown(input);
58
- const activePath = path.join(activeRunDir, 'run.md');
59
- await writeFile(activePath, content, 'utf-8');
60
- await writeRunMeta(activeRunDir, input);
49
+ await writeOneShotRunMarkdown(cwd, activeRunDir, content);
50
+ await writeRunMeta(cwd, activeRunDir, input);
61
51
  if (input.result.ok) {
62
- const completedPath = await resolveUniqueRunPath(completedDir, path.basename(activeRunDir));
63
- await moveRunDirectory(activeRunDir, completedPath);
52
+ const completedPath = await transferOneShotRunToCompleted(cwd, activeRunDir, path.basename(activeRunDir));
64
53
  return { logPath: path.join(completedPath, 'run.md'), completed: true };
65
54
  }
66
- const failedPath = await resolveUniqueRunPath(failedDir, path.basename(activeRunDir));
67
55
  if (input.result.status === 'failed' || input.result.status === 'timeout' || input.result.status === 'cancelled') {
68
- await moveRunDirectory(activeRunDir, failedPath);
56
+ const failedPath = await transferOneShotRunToFailed(cwd, activeRunDir, path.basename(activeRunDir));
69
57
  return { logPath: path.join(failedPath, 'run.md'), completed: false };
70
58
  }
71
- return { logPath: activePath, completed: false };
59
+ return { logPath: path.join(activeRunDir, 'run.md'), completed: false };
72
60
  }
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) {
61
+ async function writeRunMeta(repoRoot, runDir, input, statusOverride) {
84
62
  const runId = path.basename(runDir);
85
63
  const status = statusOverride ?? (input.result.ok ? 'completed' : input.result.status);
86
- await writeFile(path.join(runDir, 'meta.json'), `${JSON.stringify({
64
+ await writeOneShotRunMeta(repoRoot, runDir, {
87
65
  runId,
88
66
  taskId: input.taskId,
89
67
  status,
@@ -95,7 +73,7 @@ async function writeRunMeta(runDir, input, statusOverride) {
95
73
  failureCategory: input.result.failureCategory,
96
74
  artifactsDir: 'artifacts',
97
75
  logPath: 'run.md',
98
- }, null, 2)}\n`, 'utf-8');
76
+ });
99
77
  }
100
78
  function isRunMarkdownPath(logPath) {
101
79
  return path.basename(logPath) === 'run.md';
@@ -119,16 +97,10 @@ export async function createActiveCursorRun(cwd, input) {
119
97
  const datePrefix = new Date().toISOString().slice(0, 10);
120
98
  const slug = deriveRunSlug(input.task);
121
99
  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');
100
+ const activeRunDir = await createOneShotActiveRunDir(cwd, baseName);
129
101
  const content = buildActiveRunLogMarkdown(input);
130
- await writeFile(activePath, content, 'utf-8');
131
- await writeRunMeta(activeRunDir, {
102
+ await writeOneShotRunMarkdown(cwd, activeRunDir, content);
103
+ await writeRunMeta(cwd, activeRunDir, {
132
104
  taskId: input.taskId,
133
105
  task: input.task,
134
106
  model: input.model,
@@ -144,7 +116,7 @@ export async function createActiveCursorRun(cwd, input) {
144
116
  stderr: '',
145
117
  },
146
118
  }, 'running');
147
- return { logPath: activePath, taskId: input.taskId };
119
+ return { logPath: path.join(activeRunDir, 'run.md'), taskId: input.taskId };
148
120
  }
149
121
  /**
150
122
  * Finalize an active run log with execution results.
@@ -157,30 +129,23 @@ export async function finalizeCursorRunLog(logPath, input) {
157
129
  const existing = await readFile(logPath, 'utf-8').catch(() => '');
158
130
  const taskId = existing ? parseCursorRunLog(existing, logPath).taskId : undefined;
159
131
  const content = buildRunLogMarkdown({ ...input, taskId });
160
- await writeFile(logPath, content, 'utf-8');
161
132
  const activeRunDir = runDirForLogPath(logPath);
162
133
  if (activeRunDir) {
163
- await writeRunMeta(activeRunDir, { ...input, taskId });
134
+ await writeOneShotRunMarkdown(input.cwd, activeRunDir, content);
135
+ await writeRunMeta(input.cwd, activeRunDir, { ...input, taskId });
136
+ }
137
+ else {
138
+ await writeOneShotActiveLogAtPath(input.cwd, logPath, content);
164
139
  }
165
- const targetLifecycle = input.result.ok ? 'completed' : 'failed';
166
- const targetDir = path.join(harnessDir, 'runs', targetLifecycle);
167
- await mkdir(targetDir, { recursive: true });
168
140
  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 };
141
+ const transfer = input.result.ok ? transferOneShotRunToCompleted : transferOneShotRunToFailed;
142
+ const destDir = await transfer(input.cwd, path.dirname(logPath), path.basename(path.dirname(logPath)));
143
+ return { logPath: path.join(destDir, 'run.md'), completed: input.result.ok };
172
144
  }
173
145
  if (!input.result.ok) {
174
146
  return { logPath, completed: false };
175
147
  }
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
- }
148
+ const completedPath = await finalizeLegacyRunLogToCompleted(input.cwd, logPath, content);
184
149
  return { logPath: completedPath, completed: true };
185
150
  }
186
151
  /**
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { getArtifactPath } from '../shared/artifacts-core.js';
3
+ import { getArtifactPath, writeArtifactForStep } from '../shared/artifacts-core.js';
4
4
  import { getDagRunDir, readDagRunState } from '../workflows/dag/lifecycle.js';
5
5
  import { parseCursorRunLog, resolveRunStatus } from './one-shot-runs.js';
6
6
  import { getTaskDir } from '../task/runtime.js';
@@ -52,9 +52,8 @@ async function writePromotion(repoRoot, taskId, runId, sourceKind, modifyLog, ve
52
52
  const taskDir = getTaskDir(repoRoot, taskId);
53
53
  const modifyLogPath = getArtifactPath(taskDir, 'implement');
54
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');
55
+ await writeArtifactForStep(taskDir, 'implement', modifyLog);
56
+ await writeArtifactForStep(taskDir, 'verify', verifyResult);
58
57
  return { taskId, runId, sourceKind, modifyLogPath, verifyResultPath };
59
58
  }
60
59
  async function loadCompletedDagEvidence(repoRoot, runId) {
@@ -1,5 +1,5 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
2
1
  import path from 'node:path';
2
+ import { resolveTaskContextFromDir, writeTaskArtifactFile, } from '../infrastructure/harness/task-store.js';
3
3
  export const ARTIFACTS_DIR = 'artifacts';
4
4
  const SPEC_ARTIFACT_STEPS = ['analyze', 'plan'];
5
5
  const DEFAULT_ARTIFACTS = {
@@ -42,15 +42,15 @@ const REQUIRED_HEADINGS = {
42
42
  retrospective: ['# 复盘报告', '## 1. 质量复盘'],
43
43
  };
44
44
  export async function initializeArtifacts(taskDir) {
45
- const artifactsDir = path.join(taskDir, 'artifacts');
46
- await mkdir(artifactsDir, { recursive: true });
47
- await Promise.all(Object.entries(DEFAULT_ARTIFACTS).map(([name, content]) => writeFile(path.join(artifactsDir, name), content, 'utf-8')));
45
+ const { repoRoot, taskId } = resolveTaskContextFromDir(taskDir);
46
+ await Promise.all(Object.entries(DEFAULT_ARTIFACTS).map(([name, content]) => writeTaskArtifactFile(repoRoot, taskId, name, content)));
48
47
  }
49
48
  export function getArtifactPath(taskDir, step) {
50
49
  return path.join(taskDir, ARTIFACTS_DIR, getArtifactFileName(step));
51
50
  }
52
51
  export async function writeArtifactForStep(taskDir, step, content) {
53
- await writeFile(getArtifactPath(taskDir, step), `${content.trim()}\n`, 'utf-8');
52
+ const { repoRoot, taskId } = resolveTaskContextFromDir(taskDir);
53
+ await writeTaskArtifactFile(repoRoot, taskId, getArtifactFileName(step), `${content.trim()}\n`);
54
54
  }
55
55
  function normalizeArtifactContent(content) {
56
56
  return content.replace(/\r/g, '').trim().replace(/\s+/g, ' ');
@@ -1,23 +1,17 @@
1
- import { appendFile, mkdir } from 'node:fs/promises';
2
- import path from 'node:path';
1
+ import { appendTaskLogJsonlRecord, appendTaskWorkflowLog, resolveTaskContextFromDir, } from '../infrastructure/harness/task-store.js';
3
2
  export async function appendWorkflowLog(taskDir, message) {
4
- const logsDir = path.join(taskDir, 'logs');
5
- await mkdir(logsDir, { recursive: true });
6
- const line = `[${new Date().toISOString()}] ${message}\n`;
7
- await appendFile(path.join(logsDir, 'workflow.log'), line, 'utf-8');
3
+ const { repoRoot, taskId } = resolveTaskContextFromDir(taskDir);
4
+ await appendTaskWorkflowLog(repoRoot, taskId, message);
8
5
  }
9
6
  export async function appendExecutorRecord(taskDir, record) {
10
- const logsDir = path.join(taskDir, 'logs');
11
- await mkdir(logsDir, { recursive: true });
12
- await appendFile(path.join(logsDir, 'executor.jsonl'), `${JSON.stringify(record)}\n`, 'utf-8');
7
+ const { repoRoot, taskId } = resolveTaskContextFromDir(taskDir);
8
+ await appendTaskLogJsonlRecord(repoRoot, taskId, 'executor.jsonl', record);
13
9
  }
14
10
  export async function appendDecisionRecord(taskDir, record) {
15
- const logsDir = path.join(taskDir, 'logs');
16
- await mkdir(logsDir, { recursive: true });
17
- await appendFile(path.join(logsDir, 'decisions.jsonl'), `${JSON.stringify(record)}\n`, 'utf-8');
11
+ const { repoRoot, taskId } = resolveTaskContextFromDir(taskDir);
12
+ await appendTaskLogJsonlRecord(repoRoot, taskId, 'decisions.jsonl', record);
18
13
  }
19
14
  export async function appendGoalEventRecord(taskDir, record) {
20
- const logsDir = path.join(taskDir, 'logs');
21
- await mkdir(logsDir, { recursive: true });
22
- await appendFile(path.join(logsDir, 'goal-events.jsonl'), `${JSON.stringify(record)}\n`, 'utf-8');
15
+ const { repoRoot, taskId } = resolveTaskContextFromDir(taskDir);
16
+ await appendTaskLogJsonlRecord(repoRoot, taskId, 'goal-events.jsonl', record);
23
17
  }