@tea-agent/loop-agent 0.7.5 → 0.9.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 (133) hide show
  1. package/AGENTS.md +145 -142
  2. package/CHANGELOG.md +161 -158
  3. package/README.md +206 -204
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/args.js +9 -2
  7. package/dist/commands/init.js +518 -488
  8. package/dist/commands/loop-benchmark.js +11 -11
  9. package/dist/commands/pi-reuse-benchmark.js +16 -16
  10. package/dist/executors/cursor-executor.js +1 -1
  11. package/dist/executors/shell-executor.js +74 -8
  12. package/dist/governance/manifest-types.js +1 -1
  13. package/dist/shared/reference-context.js +48 -22
  14. package/dist/task/config-types.js +1 -1
  15. package/dist/task/runtime.js +28 -28
  16. package/dist/worker/cli.js +3 -3
  17. package/dist/worker/loop-agent/loop-agent-client.js +51 -10
  18. package/dist/worker/observability/event-store.js +2 -1
  19. package/dist/worker/observability/read-model.js +13 -11
  20. package/dist/worker/observe/paths.js +2 -2
  21. package/dist/worker/observe/routes.js +82 -23
  22. package/dist/worker/observe/server.js +8 -6
  23. package/dist/worker/observe/static/app.js +1965 -1480
  24. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  25. package/dist/worker/observe/static/dag-layout.js +83 -83
  26. package/dist/worker/observe/static/index.html +88 -63
  27. package/dist/worker/observe/static/styles.css +598 -722
  28. package/dist/worker/pool/run-store.js +7 -8
  29. package/dist/worker/run-task/run-task.js +11 -2
  30. package/dist/worker/runner/run-ready.js +1 -1
  31. package/dist/workflows/dag/canvas-observer.js +275 -275
  32. package/docs/README.md +84 -79
  33. package/docs/agent-dag-recovery-playbook.md +184 -184
  34. package/docs/agent-dag-runner.md +42 -42
  35. package/docs/architecture/runtime-boundaries.md +162 -162
  36. package/docs/cursor-executor-usage.md +25 -25
  37. package/docs/decisions/README.md +3 -3
  38. package/docs/design/README.md +49 -49
  39. package/docs/development-principles.md +73 -73
  40. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  41. package/docs/exec-plans/README.md +6 -6
  42. package/docs/exec-plans/active/README.md +11 -11
  43. package/docs/exec-plans/completed/README.md +43 -34
  44. package/docs/feature-workflow.md +187 -187
  45. package/docs/harness-methodology-debugging.md +153 -153
  46. package/docs/harness-methodology-tdd.md +130 -130
  47. package/docs/harness-methodology-verification.md +27 -27
  48. package/docs/init-surface.manifest.json +245 -241
  49. package/docs/loop-agent-harness.md +63 -55
  50. package/docs/production-readiness.md +96 -96
  51. package/docs/progress/README.md +3 -3
  52. package/docs/reports/README.md +12 -9
  53. package/docs/skills/README.md +6 -6
  54. package/docs/skills/vetted-skill-registry.md +26 -26
  55. package/docs/templates/adr.md +60 -60
  56. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  57. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  58. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  59. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  60. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  61. package/docs/templates/agent-dag-report.schema.json +454 -454
  62. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  63. package/docs/templates/agent-dag.base.json +195 -195
  64. package/docs/templates/agent-dag.final-verification.json +190 -190
  65. package/docs/templates/agent-dag.schema.json +316 -316
  66. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  67. package/docs/templates/exec-plan.md +64 -64
  68. package/docs/templates/feature-spec.md +53 -53
  69. package/docs/templates/harness.schema.json +218 -0
  70. package/docs/templates/hybrid-dag.json +193 -193
  71. package/docs/templates/init-evolution-review.md +33 -33
  72. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  73. package/docs/templates/product-line/AGENTS.md +8 -8
  74. package/docs/templates/product-line/README.md +9 -9
  75. package/docs/templates/product-line/acceptance.yaml +14 -14
  76. package/docs/templates/product-line/closeout.yaml +9 -9
  77. package/docs/templates/product-line/design.md +13 -13
  78. package/docs/templates/product-line/links.md +10 -10
  79. package/docs/templates/product-line/requirement.md +17 -17
  80. package/docs/templates/product-line/task-graph.yaml +15 -15
  81. package/docs/templates/product-line/task.yaml +65 -65
  82. package/docs/templates/product-line/test-plan.md +7 -7
  83. package/docs/templates/production-readiness-checklist.md +57 -57
  84. package/docs/templates/progress-log.md +17 -17
  85. package/docs/templates/project-start-checklist.md +9 -9
  86. package/docs/templates/qa-report.md +48 -48
  87. package/docs/templates/sprint-contract.md +29 -29
  88. package/docs/templates/worker-dogfood-evidence.md +52 -52
  89. package/docs/templates/worker-dogfood-setup.md +48 -48
  90. package/docs/verification-matrix.md +49 -49
  91. package/examples/decision-gate-agent-dag.json +123 -123
  92. package/examples/example-dag.json +51 -51
  93. package/examples/hybrid-loop-agent-dag.json +194 -194
  94. package/harness.json +73 -71
  95. package/package.json +68 -67
  96. package/scripts/check-product-line-docs.sh +22 -22
  97. package/scripts/check-task-pool-root.sh +32 -0
  98. package/skills/ai-engineering-context/SKILL.md +48 -48
  99. package/skills/code-review-core/SKILL.md +20 -20
  100. package/skills/codebase-scout/SKILL.md +19 -19
  101. package/skills/init-capability-evolution/SKILL.md +69 -69
  102. package/skills/loop-agent/SKILL.md +149 -149
  103. package/skills/loop-agent/references/README.md +67 -67
  104. package/skills/loop-agent/references/command-reference.md +432 -412
  105. package/skills/loop-agent/references/harness-policy.md +263 -263
  106. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  107. package/skills/loop-agent/references/learned/README.md +21 -21
  108. package/skills/loop-agent/references/long-running-loop.md +59 -59
  109. package/skills/loop-agent/references/model-routing.md +36 -36
  110. package/skills/loop-agent/references/multi-worktree.md +54 -54
  111. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  112. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  113. package/skills/loop-agent/references/pi-prompt.md +23 -23
  114. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  115. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  116. package/skills/loop-agent/references/task-workflow.md +89 -89
  117. package/skills/loop-agent/references/verification-and-failure-handling.md +133 -128
  118. package/skills/requesting-code-review/SKILL.md +101 -101
  119. package/skills/requesting-code-review/code-reviewer.md +168 -168
  120. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  121. package/skills/systematic-debugging/SKILL.md +296 -296
  122. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  123. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  124. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  125. package/skills/systematic-debugging/find-polluter.sh +63 -63
  126. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  127. package/skills/systematic-debugging/test-academic.md +14 -14
  128. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  129. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  130. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  131. package/skills/test-driven-development/SKILL.md +20 -20
  132. package/skills/verification-before-completion/SKILL.md +154 -154
  133. package/skills/webapp-testing/SKILL.md +19 -19
@@ -37,17 +37,17 @@ export function parseLoopBenchmarkArgs(args) {
37
37
  return { json, markdown, outputPath };
38
38
  }
39
39
  export function printLoopBenchmarkUsage() {
40
- console.log(`usage: loop-benchmark [options]
41
-
42
- Deterministic loop-agent loop benchmark baseline (no live Pi/Cursor calls).
43
-
44
- Options:
45
- --json Emit JSON (default when no format flag is set)
46
- --markdown Emit Markdown report
47
- --output <path> Write Markdown report to a repo-relative or absolute path
48
- -h, --help Show this help
49
-
50
- Control groups: single-repair, 3-pass-convergence, 3-pass-convergence+quota.
40
+ console.log(`usage: loop-benchmark [options]
41
+
42
+ Deterministic loop-agent loop benchmark baseline (no live Pi/Cursor calls).
43
+
44
+ Options:
45
+ --json Emit JSON (default when no format flag is set)
46
+ --markdown Emit Markdown report
47
+ --output <path> Write Markdown report to a repo-relative or absolute path
48
+ -h, --help Show this help
49
+
50
+ Control groups: single-repair, 3-pass-convergence, 3-pass-convergence+quota.
51
51
  Recommendation never changes convergence.enabled default.`);
52
52
  }
53
53
  export async function runLoopBenchmark(repoRoot, rawArgs) {
@@ -106,22 +106,22 @@ export function parsePiReuseBenchmarkArgs(args) {
106
106
  };
107
107
  }
108
108
  export function printPiReuseBenchmarkUsage() {
109
- console.log(`usage: pi-reuse-benchmark [options]
110
-
111
- Deterministic Pi runtime reuse benchmark/decision summary (no live Pi calls).
112
-
113
- Options:
114
- --report <path> Benchmark report markdown (approval status)
115
- --approval <path> Explicit approval JSON artifact
116
- --off-executor <path> Baseline executor.jsonl (reuse off)
117
- --on-executor <path> Treatment executor.jsonl (reuse on)
118
- --off-task <task-id> Resolve baseline from .harness/tasks/<id>/logs/executor.jsonl
119
- --on-task <task-id> Resolve treatment from .harness/tasks/<id>/logs/executor.jsonl
120
- --json Emit JSON (default when no format flag is set)
121
- --markdown Emit Markdown summary
122
- -h, --help Show this help
123
-
124
- Recommendations: defer | maintain-opt-in | eligible-for-human-review
109
+ console.log(`usage: pi-reuse-benchmark [options]
110
+
111
+ Deterministic Pi runtime reuse benchmark/decision summary (no live Pi calls).
112
+
113
+ Options:
114
+ --report <path> Benchmark report markdown (approval status)
115
+ --approval <path> Explicit approval JSON artifact
116
+ --off-executor <path> Baseline executor.jsonl (reuse off)
117
+ --on-executor <path> Treatment executor.jsonl (reuse on)
118
+ --off-task <task-id> Resolve baseline from .harness/tasks/<id>/logs/executor.jsonl
119
+ --on-task <task-id> Resolve treatment from .harness/tasks/<id>/logs/executor.jsonl
120
+ --json Emit JSON (default when no format flag is set)
121
+ --markdown Emit Markdown summary
122
+ -h, --help Show this help
123
+
124
+ Recommendations: defer | maintain-opt-in | eligible-for-human-review
125
125
  Never changes CODE_AGENT_PI_REUSE_RUNTIME default (off).`);
126
126
  }
127
127
  function resolveRepoRelative(repoRoot, filePath) {
@@ -29,7 +29,7 @@ export function resolveArtifactWriteDir(options) {
29
29
  }
30
30
  export function buildArtifactPathPrompt(writeDir) {
31
31
  if (!writeDir)
32
- return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
32
+ return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
33
33
  ${ARTIFACT_INSTRUCTIONS}`;
34
34
  return [
35
35
  `After changes, write the following files:`,
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { writeDagNodeTextArtifact } from "../infrastructure/harness/artifact-store.js";
5
5
  import { truncateOutput } from "./cursor-executor.js";
@@ -11,6 +11,7 @@ const SUMMARY_STDOUT_MAX = 4_000;
11
11
  const SUMMARY_STDERR_MAX = 2_000;
12
12
  const NODE_STDOUT_MAX = 12_000;
13
13
  const NODE_STDERR_MAX = 8_000;
14
+ const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
14
15
  function resolveBashExecutable() {
15
16
  const envCandidate = process.env.LOOP_AGENT_BASH ?? process.env.GIT_BASH;
16
17
  if (envCandidate && existsSync(envCandidate))
@@ -85,8 +86,8 @@ export async function executeShellCommand(input) {
85
86
  env: buildShellProcessEnv(input.envAllowlist, injectedEnv),
86
87
  stdio: ["ignore", "pipe", "pipe"],
87
88
  });
88
- let stdout = "";
89
- let stderr = "";
89
+ let stdout = createBoundedOutput();
90
+ let stderr = createBoundedOutput();
90
91
  let timedOut = false;
91
92
  let settled = false;
92
93
  let timeoutHandle;
@@ -103,22 +104,36 @@ export async function executeShellCommand(input) {
103
104
  command: input.command,
104
105
  cwd: input.cwd,
105
106
  durationMs: Date.now() - startedAt,
106
- stdout,
107
- stderr,
107
+ stdout: formatBoundedOutput(stdout),
108
+ stdoutBytes: stdout.bytes,
109
+ stdoutTruncated: stdout.truncated,
110
+ stderr: formatBoundedOutput(stderr),
111
+ stderrBytes: stderr.bytes,
112
+ stderrTruncated: stderr.truncated,
113
+ ...(input.outputArtifacts
114
+ ? {
115
+ stdoutArtifactPath: input.outputArtifacts.stdoutPath,
116
+ stderrArtifactPath: input.outputArtifacts.stderrPath,
117
+ }
118
+ : {}),
108
119
  timedOut,
109
120
  ...result,
110
121
  });
111
122
  }
112
123
  child.stdout.setEncoding("utf-8");
113
124
  child.stderr.setEncoding("utf-8");
125
+ initializeOutputArtifacts(input.outputArtifacts);
114
126
  child.stdout.on("data", (chunk) => {
115
- stdout += chunk;
127
+ stdout = appendBoundedOutput(stdout, chunk);
128
+ appendOutputArtifact(input.outputArtifacts?.stdoutPath, chunk);
116
129
  });
117
130
  child.stderr.on("data", (chunk) => {
118
- stderr += chunk;
131
+ stderr = appendBoundedOutput(stderr, chunk);
132
+ appendOutputArtifact(input.outputArtifacts?.stderrPath, chunk);
119
133
  });
120
134
  child.on("error", (error) => {
121
- stderr += stderr ? `\n${error.message}` : error.message;
135
+ stderr = appendBoundedOutput(stderr, `${stderr.text ? "\n" : ""}${error.message}`);
136
+ appendOutputArtifact(input.outputArtifacts?.stderrPath, error.message);
122
137
  finish({
123
138
  exitCode: null,
124
139
  failureCategory: "spawn-error",
@@ -149,6 +164,46 @@ export async function executeShellCommand(input) {
149
164
  }
150
165
  });
151
166
  }
167
+ function createBoundedOutput() {
168
+ return { bytes: 0, text: "", truncated: false };
169
+ }
170
+ function appendBoundedOutput(current, chunk) {
171
+ const bytes = current.bytes + Buffer.byteLength(chunk);
172
+ const combined = current.text + chunk;
173
+ if (Buffer.byteLength(combined) <= MAX_BUFFERED_OUTPUT_BYTES) {
174
+ return { bytes, text: combined, truncated: current.truncated };
175
+ }
176
+ const tail = Buffer.from(combined)
177
+ .subarray(-MAX_BUFFERED_OUTPUT_BYTES)
178
+ .toString("utf-8")
179
+ .replace(/^\uFFFD/, "");
180
+ return { bytes, text: tail, truncated: true };
181
+ }
182
+ function formatBoundedOutput(output) {
183
+ return output.truncated ? `${output.text}\n...[truncated]` : output.text;
184
+ }
185
+ function initializeOutputArtifacts(paths) {
186
+ if (!paths)
187
+ return;
188
+ try {
189
+ mkdirSync(path.dirname(paths.stdoutPath), { recursive: true });
190
+ writeFileSync(paths.stdoutPath, "", "utf-8");
191
+ writeFileSync(paths.stderrPath, "", "utf-8");
192
+ }
193
+ catch {
194
+ // Artifact persistence is best-effort; the bounded result remains available.
195
+ }
196
+ }
197
+ function appendOutputArtifact(filePath, chunk) {
198
+ if (!filePath)
199
+ return;
200
+ try {
201
+ appendFileSync(filePath, chunk, "utf-8");
202
+ }
203
+ catch {
204
+ // Keep command execution independent from a transient artifact write failure.
205
+ }
206
+ }
152
207
  function summarizeCommandResults(results) {
153
208
  const stdout = results
154
209
  .map((result) => [`$ ${result.command}`, result.stdout.trim()].filter(Boolean).join("\n"))
@@ -195,11 +250,17 @@ export function buildShellResultSummaryMarkdown(input) {
195
250
  lines.push("", `## Command ${index + 1}: ${result.command}`, "", "### stdout", "", "```text", stdout.text, "```");
196
251
  if (stdout.truncated)
197
252
  lines.push("", "_(stdout truncated)_");
253
+ if (result.stdoutArtifactPath) {
254
+ lines.push("", `Full stdout artifact: ${result.stdoutArtifactPath}`);
255
+ }
198
256
  if (stderr.text.trim()) {
199
257
  lines.push("", "### stderr", "", "```text", stderr.text, "```");
200
258
  if (stderr.truncated)
201
259
  lines.push("", "_(stderr truncated)_");
202
260
  }
261
+ if (result.stderrArtifactPath) {
262
+ lines.push("", `Full stderr artifact: ${result.stderrArtifactPath}`);
263
+ }
203
264
  }
204
265
  return `${lines.join("\n")}\n`;
205
266
  }
@@ -235,12 +296,17 @@ export async function executeDagShellNode(input, meta) {
235
296
  beforeStatus = undefined;
236
297
  }
237
298
  for (const command of commands) {
299
+ const commandNumber = results.length + 1;
238
300
  const result = await executeShellCommand({
239
301
  command,
240
302
  cwd,
241
303
  timeoutMs,
242
304
  envAllowlist: shell.envAllowlist,
243
305
  dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
306
+ outputArtifacts: {
307
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
308
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
309
+ },
244
310
  });
245
311
  results.push(result);
246
312
  if (!result.ok)
@@ -46,7 +46,7 @@ export const workflowPolicySchema = z
46
46
  .object({
47
47
  /** Preferred implementation workflow for recoverable loop-agent work. Declarative policy; callers may still require explicit CLI flags. */
48
48
  defaultImplementationWorkflow: z
49
- .enum(["agent-dag", "level-1"])
49
+ .enum(["agent-dag"])
50
50
  .optional()
51
51
  .default("agent-dag"),
52
52
  dag: z
@@ -7,6 +7,17 @@ import path from 'node:path';
7
7
  const DEFAULT_MAX_FILES_PER_REPO = 24;
8
8
  const DEFAULT_MAX_TOTAL_FILES = 48;
9
9
  const DEFAULT_MAX_FILE_BYTES = 512_000;
10
+ const MAX_REFERENCE_ENTRIES_PER_REPO = 1_024;
11
+ const SKIPPED_REFERENCE_DIRECTORIES = new Set([
12
+ '.git',
13
+ '.hg',
14
+ '.svn',
15
+ '.venv',
16
+ 'build',
17
+ 'dist',
18
+ 'node_modules',
19
+ 'target',
20
+ ]);
10
21
  export function isFeatureStudyTask(taskConfig) {
11
22
  return taskConfig.taskKind === 'feature-study';
12
23
  }
@@ -102,8 +113,8 @@ function uniqueKeepOrder(items) {
102
113
  return result;
103
114
  }
104
115
  async function collectRepoFiles(root, focusGlobs, maxFiles) {
105
- const candidates = await listFilesRecursive(root);
106
- const matched = candidates
116
+ const candidates = await listFilesRecursive(root, MAX_REFERENCE_ENTRIES_PER_REPO);
117
+ const matched = candidates.files
107
118
  .map((absolutePath) => ({
108
119
  absolutePath,
109
120
  relativePath: path.relative(root, absolutePath).replace(/\\/g, '/'),
@@ -111,7 +122,7 @@ async function collectRepoFiles(root, focusGlobs, maxFiles) {
111
122
  .filter((entry) => focusGlobs.some((pattern) => matchGlob(pattern, entry.relativePath)))
112
123
  .sort((a, b) => a.relativePath.localeCompare(b.relativePath));
113
124
  const files = [];
114
- let truncated = false;
125
+ let truncated = candidates.truncated;
115
126
  for (const entry of matched) {
116
127
  if (files.length >= maxFiles) {
117
128
  truncated = true;
@@ -122,10 +133,6 @@ async function collectRepoFiles(root, focusGlobs, maxFiles) {
122
133
  continue;
123
134
  if (fileStat.size > DEFAULT_MAX_FILE_BYTES)
124
135
  continue;
125
- const lower = entry.relativePath.toLowerCase();
126
- if (lower.includes('/target/') || lower.includes('/node_modules/') || lower.includes('/.git/')) {
127
- continue;
128
- }
129
136
  files.push({
130
137
  relativePath: entry.relativePath,
131
138
  absolutePath: entry.absolutePath,
@@ -134,21 +141,40 @@ async function collectRepoFiles(root, focusGlobs, maxFiles) {
134
141
  }
135
142
  return { files, truncated };
136
143
  }
137
- async function listFilesRecursive(dir) {
138
- let entries;
139
- try {
140
- entries = await readdir(dir, { withFileTypes: true });
141
- }
142
- catch {
143
- return [];
144
- }
145
- const nested = await Promise.all(entries.map(async (entry) => {
146
- const target = path.join(dir, entry.name);
147
- if (entry.isDirectory())
148
- return listFilesRecursive(target);
149
- return [target];
150
- }));
151
- return nested.flat();
144
+ async function listFilesRecursive(dir, maxEntries) {
145
+ const files = [];
146
+ let visited = 0;
147
+ let truncated = false;
148
+ const walk = async (current) => {
149
+ let entries;
150
+ try {
151
+ entries = await readdir(current, { withFileTypes: true });
152
+ }
153
+ catch {
154
+ return;
155
+ }
156
+ for (const entry of entries) {
157
+ if (visited >= maxEntries) {
158
+ truncated = true;
159
+ return;
160
+ }
161
+ visited += 1;
162
+ if (entry.isDirectory() && SKIPPED_REFERENCE_DIRECTORIES.has(entry.name.toLowerCase())) {
163
+ continue;
164
+ }
165
+ const target = path.join(current, entry.name);
166
+ if (entry.isDirectory()) {
167
+ await walk(target);
168
+ if (truncated)
169
+ return;
170
+ }
171
+ else {
172
+ files.push(target);
173
+ }
174
+ }
175
+ };
176
+ await walk(dir);
177
+ return { files, truncated };
152
178
  }
153
179
  export function matchGlob(pattern, relativePath) {
154
180
  const normalizedPattern = pattern.replace(/\\/g, '/');
@@ -76,7 +76,7 @@ export const taskConfigSchema = z.object({
76
76
  complexity: taskComplexitySchema.optional().default("medium"),
77
77
  /** Explicit delivery capabilities that affect writer contracts/model tier without changing risk. */
78
78
  capabilities: z.array(taskCapabilitySchema).optional(),
79
- verifyMode: verifyModeSchema.optional().default("parallel"),
79
+ verifyMode: verifyModeSchema.optional().default("serial"),
80
80
  verifyPreset: verifyPresetSchema.optional().default("auto"),
81
81
  /** Task-contract commands that must be included in final DAG shell verification. */
82
82
  verifyCommands: z.array(taskVerifyCommandSchema).optional().default([]),
@@ -102,7 +102,7 @@ export async function createTask(repoRoot, taskId, title) {
102
102
  complexity: 'medium',
103
103
  dagFallbackReason: '',
104
104
  contextProfile: 'full',
105
- verifyMode: 'parallel',
105
+ verifyMode: 'serial',
106
106
  verifyPreset: 'auto',
107
107
  verifyCommands: [],
108
108
  verifyQuota: 'full',
@@ -237,35 +237,35 @@ export function shouldInjectSubagentGuidance(step, mode) {
237
237
  return false;
238
238
  }
239
239
  /** Advisory guidance for `analyze-plan` mode. */
240
- export const SUBAGENT_GUIDANCE_STANDARD = `<subagent_guidance>
241
- You have access to the \`subagent\` tool for lightweight delegation within this step.
242
- Use it only for read-only tasks:
243
- - Parallel scout: dispatch multiple subagents to search/read different areas simultaneously
244
- - Chain: scout -> planner (one subagent scouts, another plans based on findings)
245
- - Reviewer: have a subagent review your analysis/plan before finalizing
246
- Do NOT use subagent for writing, editing, or executing commands.
247
- Subagent output is advisory only; always verify and incorporate findings into your own output.
248
- Do NOT treat subagent results as authoritative state or artifact sources.
240
+ export const SUBAGENT_GUIDANCE_STANDARD = `<subagent_guidance>
241
+ You have access to the \`subagent\` tool for lightweight delegation within this step.
242
+ Use it only for read-only tasks:
243
+ - Parallel scout: dispatch multiple subagents to search/read different areas simultaneously
244
+ - Chain: scout -> planner (one subagent scouts, another plans based on findings)
245
+ - Reviewer: have a subagent review your analysis/plan before finalizing
246
+ Do NOT use subagent for writing, editing, or executing commands.
247
+ Subagent output is advisory only; always verify and incorporate findings into your own output.
248
+ Do NOT treat subagent results as authoritative state or artifact sources.
249
249
  </subagent_guidance>`;
250
250
  /** Strong guidance for `full` mode — prescriptive when to delegate. */
251
- export const SUBAGENT_GUIDANCE_STRONG = `<subagent_guidance>
252
- You have access to the \`subagent\` tool for lightweight delegation within this step.
253
-
254
- You SHOULD delegate to subagent scouts when:
255
- - The task requires scanning 3+ directories or comparing implementations across modules
256
- - You would otherwise need 5+ sequential read/grep calls to gather context
257
- - A reviewer subagent can independently catch scope drift before you finalize your output
258
-
259
- Delegation saves context tokens and produces better results.
260
-
261
- Allowed patterns:
262
- - Parallel scout: dispatch 2-3 subagents simultaneously to cover different file trees
263
- - Chain: scout -> planner (one subagent scouts, another plans based on findings)
264
- - Reviewer: have a subagent review your analysis/plan before finalizing
265
-
266
- Do NOT use subagent for writing, editing, or executing commands.
267
- Subagent output is advisory only; always verify and incorporate findings into your own output.
268
- Do NOT treat subagent results as authoritative state or artifact sources.
251
+ export const SUBAGENT_GUIDANCE_STRONG = `<subagent_guidance>
252
+ You have access to the \`subagent\` tool for lightweight delegation within this step.
253
+
254
+ You SHOULD delegate to subagent scouts when:
255
+ - The task requires scanning 3+ directories or comparing implementations across modules
256
+ - You would otherwise need 5+ sequential read/grep calls to gather context
257
+ - A reviewer subagent can independently catch scope drift before you finalize your output
258
+
259
+ Delegation saves context tokens and produces better results.
260
+
261
+ Allowed patterns:
262
+ - Parallel scout: dispatch 2-3 subagents simultaneously to cover different file trees
263
+ - Chain: scout -> planner (one subagent scouts, another plans based on findings)
264
+ - Reviewer: have a subagent review your analysis/plan before finalizing
265
+
266
+ Do NOT use subagent for writing, editing, or executing commands.
267
+ Subagent output is advisory only; always verify and incorporate findings into your own output.
268
+ Do NOT treat subagent results as authoritative state or artifact sources.
269
269
  </subagent_guidance>`;
270
270
  /** Preserved for backward compatibility (alias of STANDARD). */
271
271
  export const SUBAGENT_GUIDANCE = SUBAGENT_GUIDANCE_STANDARD;
@@ -13,7 +13,7 @@ import { createCompositeProgressReporter } from "./observability/progress-compos
13
13
  import { createRoutedWorkerEventStore } from "./observability/event-store.js";
14
14
  import { buildGlobalSnapshot } from "./observability/read-model.js";
15
15
  import { createObserveServer } from "./observe/server.js";
16
- import { prepareTaskPoolRetry } from "./pool/run-store.js";
16
+ import { getTaskPoolRoot, prepareTaskPoolRetry } from "./pool/run-store.js";
17
17
  import { taskSpecSchema } from "./task-spec/schema.js";
18
18
  import { validateTaskSpec } from "./task-spec/validate.js";
19
19
  import { validateFeatureTaskGraph } from "./task-graph/validate.js";
@@ -91,7 +91,7 @@ export function buildAgentWorkerProgram() {
91
91
  const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
92
92
  const client = new LoopAgentClient({
93
93
  loopAgentBin: options.loopAgentBin,
94
- artifactRoot: path.join(repoRoot, ".task-pool", "artifacts", batchRunId),
94
+ artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", batchRunId),
95
95
  });
96
96
  let progress;
97
97
  try {
@@ -161,7 +161,7 @@ export function buildAgentWorkerProgram() {
161
161
  .action(async (options) => {
162
162
  const repoRoot = path.resolve(options.repo);
163
163
  const outputPath = options.output ??
164
- path.join(repoRoot, ".task-pool", "reports", "morning-report.md");
164
+ path.join(getTaskPoolRoot(repoRoot), "reports", "morning-report.md");
165
165
  await writeMorningReport({
166
166
  repoRoot,
167
167
  ...(options.batchRunId ? { batchRunId: options.batchRunId } : {}),
@@ -1,10 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { appendFileSync, writeFileSync } from "node:fs";
3
- import { mkdir, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { parseCommandJson } from "./parse-json.js";
6
6
  export const DEFAULT_WORKER_COMMAND_TIMEOUT_MS = 120_000;
7
7
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
8
+ const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
8
9
  export class LoopAgentClient {
9
10
  loopAgentBin;
10
11
  baseArgs;
@@ -35,7 +36,7 @@ export class LoopAgentClient {
35
36
  const stdoutPath = path.join(artifactDir, "stdout.txt");
36
37
  const stderrPath = path.join(artifactDir, "stderr.txt");
37
38
  const resultPath = path.join(artifactDir, "result.json");
38
- const { stdout, stderr, exitCode, timedOut } = await spawnCommand({
39
+ const { stdout, stdoutBytes, stdoutTruncated, stderr, stderrBytes, stderrTruncated, exitCode, timedOut, } = await spawnCommand({
39
40
  command,
40
41
  args: commandArgs,
41
42
  cwd: options.cwd,
@@ -58,7 +59,11 @@ export class LoopAgentClient {
58
59
  durationMs: Date.now() - startedAt,
59
60
  exitCode,
60
61
  stdout,
62
+ stdoutBytes,
63
+ stdoutTruncated,
61
64
  stderr,
65
+ stderrBytes,
66
+ stderrTruncated,
62
67
  timedOut,
63
68
  artifacts: {
64
69
  dir: artifactDir,
@@ -68,7 +73,10 @@ export class LoopAgentClient {
68
73
  },
69
74
  };
70
75
  if (options.expectJson) {
71
- const parsed = parseCommandJson(stdout);
76
+ const jsonSource = stdoutTruncated
77
+ ? await readFullOutputArtifact(stdoutPath, stdout)
78
+ : stdout;
79
+ const parsed = parseCommandJson(jsonSource);
72
80
  if (parsed.ok) {
73
81
  result.json = parsed.value;
74
82
  }
@@ -77,8 +85,6 @@ export class LoopAgentClient {
77
85
  result.parseFailure = parsed.failure;
78
86
  }
79
87
  }
80
- await writeFile(result.artifacts.stdoutPath, stdout, "utf-8");
81
- await writeFile(result.artifacts.stderrPath, stderr, "utf-8");
82
88
  await writeFile(result.artifacts.resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
83
89
  return result;
84
90
  }
@@ -109,8 +115,8 @@ function spawnCommand(input) {
109
115
  resultPath: input.resultPath,
110
116
  },
111
117
  });
112
- let stdout = "";
113
- let stderr = "";
118
+ let stdout = createBoundedOutput();
119
+ let stderr = createBoundedOutput();
114
120
  let timedOut = false;
115
121
  const timeout = setTimeout(() => {
116
122
  timedOut = true;
@@ -132,12 +138,12 @@ function spawnCommand(input) {
132
138
  child.stdout.setEncoding("utf8");
133
139
  child.stderr.setEncoding("utf8");
134
140
  child.stdout.on("data", (chunk) => {
135
- stdout += chunk;
141
+ stdout = appendBoundedOutput(stdout, chunk);
136
142
  appendChunkBestEffort(input.stdoutPath, chunk);
137
143
  invokeChunkCallback(input.onStdout, chunk);
138
144
  });
139
145
  child.stderr.on("data", (chunk) => {
140
- stderr += chunk;
146
+ stderr = appendBoundedOutput(stderr, chunk);
141
147
  appendChunkBestEffort(input.stderrPath, chunk);
142
148
  invokeChunkCallback(input.onStderr, chunk);
143
149
  });
@@ -147,10 +153,45 @@ function spawnCommand(input) {
147
153
  });
148
154
  child.on("close", (exitCode) => {
149
155
  clearTimers();
150
- resolve({ stdout, stderr, exitCode, timedOut });
156
+ resolve({
157
+ stdout: formatBoundedOutput(stdout),
158
+ stdoutBytes: stdout.bytes,
159
+ stdoutTruncated: stdout.truncated,
160
+ stderr: formatBoundedOutput(stderr),
161
+ stderrBytes: stderr.bytes,
162
+ stderrTruncated: stderr.truncated,
163
+ exitCode,
164
+ timedOut,
165
+ });
151
166
  });
152
167
  });
153
168
  }
169
+ function createBoundedOutput() {
170
+ return { bytes: 0, text: "", truncated: false };
171
+ }
172
+ function appendBoundedOutput(current, chunk) {
173
+ const bytes = current.bytes + Buffer.byteLength(chunk);
174
+ const combined = current.text + chunk;
175
+ if (Buffer.byteLength(combined) <= MAX_BUFFERED_OUTPUT_BYTES) {
176
+ return { bytes, text: combined, truncated: current.truncated };
177
+ }
178
+ const tail = Buffer.from(combined)
179
+ .subarray(-MAX_BUFFERED_OUTPUT_BYTES)
180
+ .toString("utf-8")
181
+ .replace(/^\uFFFD/, "");
182
+ return { bytes, text: tail, truncated: true };
183
+ }
184
+ function formatBoundedOutput(output) {
185
+ return output.truncated ? `${output.text}\n...[truncated]` : output.text;
186
+ }
187
+ async function readFullOutputArtifact(filePath, fallback) {
188
+ try {
189
+ return await readFile(filePath, "utf-8");
190
+ }
191
+ catch {
192
+ return fallback;
193
+ }
194
+ }
154
195
  function appendChunkBestEffort(filePath, chunk) {
155
196
  try {
156
197
  appendFileSync(filePath, chunk, "utf-8");
@@ -1,6 +1,7 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { getTaskPoolRoot } from "../pool/run-store.js";
4
5
  function writeDiagnostic(context, error) {
5
6
  const message = error instanceof Error ? error.message : String(error);
6
7
  process.stderr.write(`[worker-event-store] ${context}: ${message}\n`);
@@ -43,7 +44,7 @@ export function createWorkerEventStore(jsonlPath) {
43
44
  };
44
45
  }
45
46
  export function createRoutedWorkerEventStore(repoRoot) {
46
- const observabilityRoot = path.join(path.resolve(repoRoot), ".task-pool", "observability");
47
+ const observabilityRoot = path.join(getTaskPoolRoot(path.resolve(repoRoot)), "observability");
47
48
  const globalStore = createWorkerEventStore(path.join(observabilityRoot, "events.jsonl"));
48
49
  let queue = Promise.resolve();
49
50
  function storesFor(event) {