@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
@@ -1,4 +1,5 @@
1
- import { appendLoopSignal, appendLoopRound, generateLoopCloseout, initializeLoop, loadLoopState, parseLoopSignalType, rewriteLoopContext, runLoopAuto, runLoopCursorFix, runLoopDagAction, runLoopPiReview, runLoopShellVerification, runLoopWorkflowAction, } from "../workflows/loop/index.js";
1
+ import { appendLoopSignal, appendLoopRound, generateLoopCloseout, initializeLoop, loadLoopState, parseLoopSignalType, rewriteLoopContext, runLoopAuto, } from "../workflows/loop/index.js";
2
+ import { runLoopAction } from "../application/loop/run-action.js";
2
3
  function usage() {
3
4
  return [
4
5
  "usage: loop <init|status|run|record-round|add-signal|closeout> <task-id> [options]",
@@ -159,20 +160,30 @@ export async function runLoop(repoRoot, args) {
159
160
  timeoutMs = parsed;
160
161
  }
161
162
  if (action === "pi-review") {
162
- const record = await runLoopPiReview(repoRoot, taskId, {
163
- timeoutMs,
164
- cwd: readFlag(rest, "--cwd"),
165
- provider: readFlag(rest, "--provider"),
166
- model: readFlag(rest, "--model"),
163
+ const record = await runLoopAction({
164
+ action,
165
+ repoRoot,
166
+ taskId,
167
+ options: {
168
+ timeoutMs,
169
+ cwd: readFlag(rest, "--cwd"),
170
+ provider: readFlag(rest, "--provider"),
171
+ model: readFlag(rest, "--model"),
172
+ },
167
173
  });
168
174
  console.log(JSON.stringify(record, null, 2));
169
175
  return;
170
176
  }
171
177
  if (action === "cursor-fix") {
172
- const record = await runLoopCursorFix(repoRoot, taskId, {
173
- timeoutMs,
174
- cwd: readFlag(rest, "--cwd"),
175
- model: readFlag(rest, "--model"),
178
+ const record = await runLoopAction({
179
+ action,
180
+ repoRoot,
181
+ taskId,
182
+ options: {
183
+ timeoutMs,
184
+ cwd: readFlag(rest, "--cwd"),
185
+ model: readFlag(rest, "--model"),
186
+ },
176
187
  });
177
188
  console.log(JSON.stringify(record, null, 2));
178
189
  return;
@@ -187,13 +198,18 @@ export async function runLoop(repoRoot, args) {
187
198
  }
188
199
  maxConcurrent = parsed;
189
200
  }
190
- const record = await runLoopDagAction(repoRoot, taskId, {
191
- mode: rest.includes("--execute") ? "execute" : "review",
192
- cwd: readFlag(rest, "--cwd"),
193
- dagPath: readFlag(rest, "--dag"),
194
- runId: readFlag(rest, "--run-id"),
195
- maxConcurrent,
196
- noCursor: rest.includes("--no-cursor"),
201
+ const record = await runLoopAction({
202
+ action,
203
+ repoRoot,
204
+ taskId,
205
+ options: {
206
+ mode: rest.includes("--execute") ? "execute" : "review",
207
+ cwd: readFlag(rest, "--cwd"),
208
+ dagPath: readFlag(rest, "--dag"),
209
+ runId: readFlag(rest, "--run-id"),
210
+ maxConcurrent,
211
+ noCursor: rest.includes("--no-cursor"),
212
+ },
197
213
  });
198
214
  console.log(JSON.stringify(record, null, 2));
199
215
  return;
@@ -212,16 +228,21 @@ export async function runLoop(repoRoot, args) {
212
228
  if (profile && profile !== "pr-review") {
213
229
  throw new Error(`unsupported workflow profile: ${profile}`);
214
230
  }
215
- const record = await runLoopWorkflowAction(repoRoot, taskId, {
216
- mode: rest.includes("--execute") ? "execute" : "review",
217
- cwd: readFlag(rest, "--cwd"),
218
- profile: profile,
219
- workflowPath: readFlag(rest, "--workflow"),
220
- runId: readFlag(rest, "--run-id"),
221
- maxConcurrent,
222
- base: readFlag(rest, "--base"),
223
- head: readFlag(rest, "--head"),
224
- changedFiles: readRepeatedFlag(rest, "--changed-file"),
231
+ const record = await runLoopAction({
232
+ action,
233
+ repoRoot,
234
+ taskId,
235
+ options: {
236
+ mode: rest.includes("--execute") ? "execute" : "review",
237
+ cwd: readFlag(rest, "--cwd"),
238
+ profile: profile,
239
+ workflowPath: readFlag(rest, "--workflow"),
240
+ runId: readFlag(rest, "--run-id"),
241
+ maxConcurrent,
242
+ base: readFlag(rest, "--base"),
243
+ head: readFlag(rest, "--head"),
244
+ changedFiles: readRepeatedFlag(rest, "--changed-file"),
245
+ },
225
246
  });
226
247
  console.log(JSON.stringify(record, null, 2));
227
248
  return;
@@ -229,10 +250,15 @@ export async function runLoop(repoRoot, args) {
229
250
  if (action !== "shell-verify") {
230
251
  throw new Error("loop run currently supports --action shell-verify|pi-review|cursor-fix|dag|workflow");
231
252
  }
232
- const record = await runLoopShellVerification(repoRoot, taskId, {
233
- commands: readRepeatedFlag(rest, "--command"),
234
- timeoutMs,
235
- cwd: readFlag(rest, "--cwd"),
253
+ const record = await runLoopAction({
254
+ action: "shell-verify",
255
+ repoRoot,
256
+ taskId,
257
+ options: {
258
+ commands: readRepeatedFlag(rest, "--command"),
259
+ timeoutMs,
260
+ cwd: readFlag(rest, "--cwd"),
261
+ },
236
262
  });
237
263
  console.log(JSON.stringify(record, null, 2));
238
264
  return;
@@ -9,17 +9,10 @@
9
9
  * pi-prompt --cwd /path --timeout 120000 "task"
10
10
  */
11
11
  import { DEFAULT_TIMEOUT_MS } from "../executors/pi-executor.js";
12
+ import { DEFAULT_PI_MODEL, DEFAULT_PI_PROVIDER, DEFAULT_PI_TOOLS, } from "../executors/pi-defaults.js";
12
13
  import { createOneShotPromptParseAccum, formatUnknownFlagError, parseOneShotCommonFlag, printOneShotHelpAndExit, resolveOneShotCwd, resolveOneShotTask, } from "../shared/one-shot-prompt-args.js";
13
14
  import { executeSingleSdkAttempt } from "../executors/pi-sdk-executor.js";
14
- export const DEFAULT_PI_PROVIDER = "wizard-local";
15
- export const DEFAULT_PI_MODEL = "glm-5.2";
16
- export const DEFAULT_PI_TOOLS = [
17
- "read",
18
- "bash",
19
- "ast_grep_search",
20
- "lsp_diagnostics",
21
- "subagent",
22
- ];
15
+ export { DEFAULT_PI_MODEL, DEFAULT_PI_PROVIDER, DEFAULT_PI_TOOLS };
23
16
  const PI_PROMPT_USAGE = [
24
17
  "usage: pi-prompt [options] [task]",
25
18
  "",
@@ -1,184 +1,11 @@
1
- import { readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { createDagCanvasObserver, resolveCanvasPath, } from "../workflows/dag/canvas-observer.js";
4
- import { loadDagSpecFromFile, runDag } from "../workflows/dag/runner.js";
5
- import { assertValidDagSpec, collectForbiddenExecutorIssues, } from "../workflows/dag/validate.js";
6
- export function parseRunDagArgs(args, defaultCwd) {
7
- if (args.length === 0) {
8
- throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]]");
9
- }
10
- let dagPath;
11
- let cwd;
12
- let initOnly = false;
13
- let dryRun = false;
14
- let maxConcurrent;
15
- let runId;
16
- let noCursor = false;
17
- let canvasPath;
18
- let canvasName;
19
- let canvasesDir;
20
- for (let i = 0; i < args.length; i += 1) {
21
- const arg = args[i];
22
- if (arg === "--dag") {
23
- dagPath = args[++i];
24
- }
25
- else if (arg.startsWith("--dag=")) {
26
- dagPath = arg.slice(6);
27
- }
28
- else if (arg === "--cwd" || arg === "-C") {
29
- cwd = args[++i];
30
- }
31
- else if (arg.startsWith("--cwd=")) {
32
- cwd = arg.slice(6);
33
- }
34
- else if (arg === "--init-only") {
35
- initOnly = true;
36
- }
37
- else if (arg === "--dry-run") {
38
- dryRun = true;
39
- }
40
- else if (arg === "--max-concurrent") {
41
- maxConcurrent = Number(args[++i]);
42
- }
43
- else if (arg === "--run-id") {
44
- runId = args[++i];
45
- }
46
- else if (arg === "--no-cursor") {
47
- noCursor = true;
48
- }
49
- else if (arg === "--canvas-path") {
50
- canvasPath = args[++i];
51
- }
52
- else if (arg.startsWith("--canvas-path=")) {
53
- canvasPath = arg.slice(14);
54
- }
55
- else if (arg === "--canvas") {
56
- canvasName = args[++i];
57
- }
58
- else if (arg.startsWith("--canvas=")) {
59
- canvasName = arg.slice(9);
60
- }
61
- else if (arg === "--canvases-dir") {
62
- canvasesDir = args[++i];
63
- }
64
- else if (arg.startsWith("--canvases-dir=")) {
65
- canvasesDir = arg.slice(15);
66
- }
67
- else if (!arg.startsWith("-")) {
68
- if (!dagPath)
69
- dagPath = arg;
70
- else
71
- throw new Error(`unexpected positional argument: ${arg}`);
72
- }
73
- else {
74
- throw new Error(`unknown run-dag flag: ${arg}`);
75
- }
76
- }
77
- if (!dagPath) {
78
- throw new Error("run-dag requires --dag <path>");
79
- }
80
- return {
81
- dagPath: path.resolve(dagPath),
82
- cwd: path.resolve(cwd ?? defaultCwd ?? process.cwd()),
83
- initOnly,
84
- dryRun,
85
- maxConcurrent,
86
- runId,
87
- noCursor,
88
- canvasPath,
89
- canvasName,
90
- canvasesDir,
91
- };
92
- }
93
- async function loadRawTaskExecutorMap(dagPath) {
94
- const raw = JSON.parse(await readFile(dagPath, "utf-8"));
95
- const map = new Map();
96
- for (const task of raw.tasks ?? []) {
97
- if (typeof task.id === "string")
98
- map.set(task.id, task.executor);
99
- }
100
- return map;
101
- }
102
- function assertNoCursorTasks(spec, rawTaskExecutors) {
103
- const issues = collectForbiddenExecutorIssues(spec, ["cursor"]);
104
- if (issues.length === 0)
105
- return;
106
- const details = issues
107
- .map((issue) => {
108
- const implicitDefault = rawTaskExecutors.get(issue.taskId) === undefined;
109
- return `task=${issue.taskId}${implicitDefault ? " (implicit default)" : ""}`;
110
- })
111
- .join("; ");
112
- throw new Error(`--no-cursor forbids cursor executor tasks: ${details}`);
113
- }
114
- function getCanvasFlushErrorMessage(error) {
115
- return error instanceof Error ? error.message : String(error);
116
- }
117
- async function flushCanvasSafely(canvas) {
118
- if (!canvas)
119
- return undefined;
120
- try {
121
- await canvas.flush();
122
- return undefined;
123
- }
124
- catch (error) {
125
- // Canvas is a derived view; failed writes must not decide canonical DAG success.
126
- return getCanvasFlushErrorMessage(error);
127
- }
128
- }
1
+ import { parseRunDagArgs } from "../application/dag/args.js";
2
+ import { runDagUseCase } from "../application/dag/run-dag.js";
3
+ export { parseRunDagArgs };
129
4
  export async function runRunDag(repoRoot, rawArgs) {
130
5
  const parsed = parseRunDagArgs(rawArgs, repoRoot);
131
- const spec = await loadDagSpecFromFile(parsed.dagPath);
132
- if (parsed.noCursor) {
133
- const rawTaskExecutors = await loadRawTaskExecutorMap(parsed.dagPath);
134
- assertNoCursorTasks(spec, rawTaskExecutors);
135
- }
136
- assertValidDagSpec(spec);
137
- const resolvedCanvasPath = resolveCanvasPath({
138
- canvasPath: parsed.canvasPath,
139
- canvasName: parsed.canvasName,
140
- canvasesDir: parsed.canvasesDir,
141
- cwd: parsed.cwd,
142
- });
143
- const canvas = resolvedCanvasPath
144
- ? createDagCanvasObserver({ canvasPath: resolvedCanvasPath, spec })
145
- : undefined;
146
- if (parsed.dryRun) {
147
- const summary = await runDag(spec, {
148
- cwd: parsed.cwd,
149
- dryRun: true,
150
- runId: parsed.runId,
151
- observer: canvas?.observer,
152
- });
153
- const canvasError = await flushCanvasSafely(canvas);
154
- console.log(JSON.stringify({
155
- mode: "dry-run",
156
- title: spec.title,
157
- taskCount: spec.tasks.length,
158
- ranks: summary.ranks,
159
- runId: summary.runId,
160
- runDir: summary.runDir,
161
- ...(resolvedCanvasPath ? { canvasPath: resolvedCanvasPath } : {}),
162
- ...(canvasError ? { canvasError } : {}),
163
- }, null, 2));
164
- return;
165
- }
166
- const maxConcurrent = parsed.maxConcurrent;
167
- const summary = await runDag(spec, {
168
- cwd: parsed.cwd,
169
- initOnly: parsed.initOnly,
170
- dryRun: false,
171
- maxConcurrent,
172
- runId: parsed.runId,
173
- observer: canvas?.observer,
6
+ const result = await runDagUseCase({
7
+ repoRoot,
8
+ ...parsed,
174
9
  });
175
- const canvasError = await flushCanvasSafely(canvas);
176
- const payload = resolvedCanvasPath
177
- ? {
178
- ...summary,
179
- canvasPath: resolvedCanvasPath,
180
- ...(canvasError ? { canvasError } : {}),
181
- }
182
- : summary;
183
- console.log(JSON.stringify(payload, null, 2));
10
+ console.log(JSON.stringify(result, null, 2));
184
11
  }
@@ -1,5 +1,5 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { writeTextArtifactFile } from "../infrastructure/harness/artifact-store.js";
3
3
  import { redactPromptForLog, truncateOutput, } from "./cursor-executor.js";
4
4
  import { formatCursorFailureMessage, } from "./cursor-execution-log.js";
5
5
  const SUMMARY_STDOUT_MAX = 4_000;
@@ -126,10 +126,9 @@ export function buildResultSummaryMarkdown(input) {
126
126
  return meta.join("\n");
127
127
  }
128
128
  export async function writeCursorExecutorArtifacts(artifactsDir, input) {
129
- await mkdir(artifactsDir, { recursive: true });
130
129
  const promptPath = path.join(artifactsDir, "prompt.redacted.md");
131
130
  const summaryPath = path.join(artifactsDir, "result.summary.md");
132
- await writeFile(promptPath, buildPromptRedactedMarkdown(input.prompt), "utf-8");
133
- await writeFile(summaryPath, buildResultSummaryMarkdown(input), "utf-8");
131
+ await writeTextArtifactFile(promptPath, buildPromptRedactedMarkdown(input.prompt));
132
+ await writeTextArtifactFile(summaryPath, buildResultSummaryMarkdown(input));
134
133
  return { promptPath, summaryPath };
135
134
  }
@@ -3,8 +3,9 @@ import { createInterface } from 'node:readline';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { DEFAULT_CURSOR_TIMEOUT_MS, } from './cursor-executor.js';
6
- const WORKER_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7
- const WORKER_ENTRY = path.join(WORKER_MODULE_DIR, '../cursor-worker-entry.ts');
6
+ const WORKER_MODULE_PATH = fileURLToPath(import.meta.url);
7
+ const WORKER_MODULE_DIR = path.dirname(WORKER_MODULE_PATH);
8
+ const WORKER_ENTRY = resolveCursorWorkerEntryPath(WORKER_MODULE_PATH);
8
9
  let workerProcess = null;
9
10
  let workerBoot = null;
10
11
  const pending = new Map();
@@ -37,8 +38,17 @@ export function shouldUseCursorWorker(options) {
37
38
  return true;
38
39
  return isCursorWorkerEnabled();
39
40
  }
41
+ export function resolveCursorWorkerEntryPath(modulePath) {
42
+ const extension = path.extname(modulePath) === '.ts' ? '.ts' : '.js';
43
+ return path.join(path.dirname(modulePath), `../cursor-worker-entry${extension}`);
44
+ }
45
+ export function buildCursorWorkerScriptArgs(entryPath) {
46
+ return path.extname(entryPath) === '.ts'
47
+ ? ['--import', 'tsx/esm', entryPath]
48
+ : [entryPath];
49
+ }
40
50
  function workerScriptArgs() {
41
- return ['--import', 'tsx/esm', WORKER_ENTRY];
51
+ return buildCursorWorkerScriptArgs(WORKER_ENTRY);
42
52
  }
43
53
  export function getCursorWorkerEntryPath() {
44
54
  return WORKER_ENTRY;
@@ -1,5 +1,5 @@
1
- import { mkdir } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { ensureDagNodeArtifactDir } from "../infrastructure/harness/artifact-store.js";
3
3
  import { executeCursorTask } from "./cursor-executor.js";
4
4
  import { writeCursorExecutorArtifacts } from "./cursor-executor-artifacts.js";
5
5
  import { buildCursorExecutionLogRecord, persistCursorExecutionLog, } from "./cursor-execution-log.js";
@@ -14,8 +14,7 @@ export async function executeDagCursorNode(input, meta) {
14
14
  // Non-git cwd: skip write guard (documented limitation).
15
15
  beforeStatus = undefined;
16
16
  }
17
- const artifactDir = path.join(meta.runDir, "artifacts", input.task.id);
18
- await mkdir(artifactDir, { recursive: true });
17
+ const artifactDir = await ensureDagNodeArtifactDir(meta.runDir, input.task.id);
19
18
  const result = await executeCursorTask({
20
19
  task: input.prompt,
21
20
  cwd: input.cwd,
@@ -1,5 +1,5 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { writeTextArtifactFile } from "../infrastructure/harness/artifact-store.js";
3
3
  import { executePiStep, } from "./pi-executor.js";
4
4
  import { redactPromptForLog, truncateOutput } from "./cursor-executor.js";
5
5
  import { readGitStatusPorcelain, runPostRunWriteGuard, } from "./shell-write-guard.js";
@@ -211,11 +211,10 @@ export function buildPiResultSummaryMarkdown(input) {
211
211
  return lines.join("\n");
212
212
  }
213
213
  export async function writePiExecutorArtifacts(artifactsDir, input) {
214
- await mkdir(artifactsDir, { recursive: true });
215
214
  const promptPath = path.join(artifactsDir, "prompt.redacted.md");
216
215
  const summaryPath = path.join(artifactsDir, "result.summary.md");
217
- await writeFile(promptPath, buildPiPromptRedactedMarkdown(input.prompt), "utf-8");
218
- await writeFile(summaryPath, buildPiResultSummaryMarkdown(input), "utf-8");
216
+ await writeTextArtifactFile(promptPath, buildPiPromptRedactedMarkdown(input.prompt));
217
+ await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
219
218
  return { promptPath, summaryPath };
220
219
  }
221
220
  export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
@@ -1,5 +1,4 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import path from "node:path";
1
+ import { writeDagNodeTextArtifact } from "../infrastructure/harness/artifact-store.js";
3
2
  export function buildStaticResultSummaryMarkdown(input) {
4
3
  const lines = [
5
4
  "# Static execution summary",
@@ -26,15 +25,13 @@ export async function executeDagStaticNode(input, meta) {
26
25
  }
27
26
  const status = staticConfig.status ?? "success";
28
27
  const started = Date.now();
29
- const nodeDir = path.join(meta.runDir, input.task.id);
30
- await mkdir(nodeDir, { recursive: true });
31
28
  const summary = buildStaticResultSummaryMarkdown({
32
29
  nodeId: input.task.id,
33
30
  runId: meta.runId,
34
31
  resultMarkdown: staticConfig.resultMarkdown,
35
32
  status,
36
33
  });
37
- await writeFile(path.join(nodeDir, "result.summary.md"), summary, "utf-8");
34
+ await writeDagNodeTextArtifact(meta.runDir, input.task.id, "result.summary.md", summary);
38
35
  return {
39
36
  ok: status === "success",
40
37
  stdout: staticConfig.resultMarkdown,
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_PI_PROVIDER = "wizard-local";
2
+ export const DEFAULT_PI_MODEL = "glm-5.2";
3
+ export const DEFAULT_PI_TOOLS = [
4
+ "read",
5
+ "bash",
6
+ "ast_grep_search",
7
+ "lsp_diagnostics",
8
+ "subagent",
9
+ ];
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
4
+ import { writeDagNodeTextArtifact } from "../infrastructure/harness/artifact-store.js";
5
5
  import { truncateOutput } from "./cursor-executor.js";
6
6
  import { expandShellPreset, buildVerdictGateShellCommand } from "./shell-presets.js";
7
7
  import { pathsChangedDuringRun, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
@@ -12,23 +12,17 @@ const SUMMARY_STDERR_MAX = 2_000;
12
12
  const NODE_STDOUT_MAX = 12_000;
13
13
  const NODE_STDERR_MAX = 8_000;
14
14
  function resolveBashExecutable() {
15
- if (process.env.LOOP_AGENT_BASH)
16
- return process.env.LOOP_AGENT_BASH;
15
+ const envCandidate = process.env.LOOP_AGENT_BASH ?? process.env.GIT_BASH;
16
+ if (envCandidate && existsSync(envCandidate))
17
+ return envCandidate;
17
18
  if (process.platform !== "win32")
18
- return "bash";
19
+ return envCandidate || "bash";
19
20
  const candidates = [
20
- process.env.GIT_BASH,
21
- process.env.ProgramFiles
22
- ? path.join(process.env.ProgramFiles, "Git", "bin", "bash.exe")
23
- : undefined,
24
- process.env["ProgramFiles(x86)"]
25
- ? path.join(process.env["ProgramFiles(x86)"], "Git", "bin", "bash.exe")
26
- : undefined,
27
- process.env.LocalAppData
28
- ? path.join(process.env.LocalAppData, "Programs", "Git", "bin", "bash.exe")
29
- : undefined,
30
- ];
31
- return candidates.find((candidate) => candidate && existsSync(candidate)) ?? "bash";
21
+ path.join(process.env.ProgramFiles ?? "C:\\Program Files", "Git", "bin", "bash.exe"),
22
+ path.join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Git", "bin", "bash.exe"),
23
+ path.join(process.env.LocalAppData ?? "", "Programs", "Git", "bin", "bash.exe"),
24
+ ].filter(Boolean);
25
+ return candidates.find((candidate) => existsSync(candidate)) ?? envCandidate ?? "bash";
32
26
  }
33
27
  function isWithinRoot(root, candidate) {
34
28
  const relative = path.relative(root, candidate);
@@ -272,15 +266,13 @@ export async function executeDagShellNode(input, meta) {
272
266
  const commandsOk = results.every((result) => result.ok);
273
267
  const ok = commandsOk && writeGuardOk;
274
268
  const firstFailure = results.find((result) => !result.ok);
275
- const nodeDir = path.join(meta.runDir, input.task.id);
276
- await mkdir(nodeDir, { recursive: true });
277
- await writeFile(path.join(nodeDir, "result.summary.md"), buildShellResultSummaryMarkdown({
269
+ await writeDagNodeTextArtifact(meta.runDir, input.task.id, "result.summary.md", buildShellResultSummaryMarkdown({
278
270
  nodeId: input.task.id,
279
271
  runId: meta.runId,
280
272
  rootCwd: input.cwd,
281
273
  results,
282
274
  writeGuardViolations: writeGuardOk ? undefined : writeGuardViolations,
283
- }), "utf-8");
275
+ }));
284
276
  const aggregate = summarizeCommandResults(results);
285
277
  const stderrParts = [aggregate.stderr];
286
278
  if (!writeGuardOk) {
@@ -83,6 +83,7 @@ export const workflowPolicySchema = z
83
83
  export const harnessManifestSchema = z.object({
84
84
  version: z.number(),
85
85
  project: z.string(),
86
+ adapter: z.string().optional(),
86
87
  governanceRoot: z.string(),
87
88
  features: z
88
89
  .object({
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ export const DAG_ACTIVE_DIR = ".harness/dag-runs/active";
3
+ export const TOOL_ACTIVE_DIR = ".harness/runs/active";
4
+ function filterRuntimeEntries(entries) {
5
+ return entries
6
+ .map((entry) => entry.trim())
7
+ .filter(Boolean)
8
+ .filter((entry) => path.basename(entry) !== ".gitkeep");
9
+ }
10
+ export function evaluateActiveResidue(input) {
11
+ const findings = [];
12
+ const dagEntries = filterRuntimeEntries(input.dagActiveEntries);
13
+ if (dagEntries.length > 0) {
14
+ findings.push({
15
+ allowEnv: "HARNESS_ALLOW_ACTIVE_DAG_RUNS",
16
+ dir: DAG_ACTIVE_DIR,
17
+ entries: dagEntries,
18
+ kind: "dag-active",
19
+ message: "[HARNESS RUNTIME DIRTY] .harness/dag-runs/active contains runtime entries:",
20
+ remediation: [
21
+ "",
22
+ ".harness/dag-runs/active is a transient runtime area and should be empty at governance check time.",
23
+ "Completed DAG runs must be archived by run-dag to .harness/dag-runs/completed/<run-id>/.",
24
+ "Paused DAG runs (decision gate requiresHuman) live under .harness/dag-runs/paused/<run-id>/ and are gitignored runtime state.",
25
+ "Dry-run/init-only/stale active directories must be removed after review.",
26
+ "",
27
+ "If a live DAG is intentionally running while this check executes, rerun with:",
28
+ " HARNESS_ALLOW_ACTIVE_DAG_RUNS=1 bash scripts/check-harness-runtime-clean.sh",
29
+ ].join("\n"),
30
+ severity: "error",
31
+ strictEnv: null,
32
+ });
33
+ }
34
+ const toolEntries = filterRuntimeEntries(input.toolActiveEntries);
35
+ if (toolEntries.length > 0) {
36
+ findings.push({
37
+ allowEnv: "HARNESS_ALLOW_ACTIVE_TOOL_RUNS",
38
+ dir: TOOL_ACTIVE_DIR,
39
+ entries: toolEntries,
40
+ kind: "tool-active",
41
+ message: "[HARNESS RUNTIME WARNING] .harness/runs/active contains runtime entries:",
42
+ remediation: [
43
+ "",
44
+ ".harness/runs/active is for live one-shot tool runs only.",
45
+ "Completed, failed, or stale one-shot run evidence must be moved or explicitly reconciled.",
46
+ "",
47
+ "If a live one-shot run is intentionally active while this check executes, rerun with:",
48
+ " HARNESS_ALLOW_ACTIVE_TOOL_RUNS=1 bash scripts/check-harness-runtime-clean.sh",
49
+ ].join("\n"),
50
+ severity: "warning",
51
+ strictEnv: "HARNESS_STRICT_ACTIVE_TOOL_RUNS",
52
+ });
53
+ }
54
+ return findings;
55
+ }
56
+ export function shouldFailActiveResidue(findings, env) {
57
+ const blocking = findings.filter((finding) => {
58
+ if (env[finding.allowEnv] === "1")
59
+ return false;
60
+ if (finding.kind === "dag-active")
61
+ return true;
62
+ return finding.strictEnv ? env[finding.strictEnv] === "1" : false;
63
+ });
64
+ return { blocking, fail: blocking.length > 0 };
65
+ }
66
+ export function formatActiveResidueFinding(finding) {
67
+ const lines = [
68
+ finding.message,
69
+ ...finding.entries.map((entry) => ` - ${entry}`),
70
+ finding.remediation,
71
+ ];
72
+ return `${lines.join("\n")}\n`;
73
+ }