@tea-agent/loop-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/AGENTS.md +121 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +144 -0
  4. package/bin/loop-agent.js +21 -0
  5. package/dist/adapters/aimax.js +91 -0
  6. package/dist/adapters/context.js +32 -0
  7. package/dist/adapters/index.js +28 -0
  8. package/dist/adapters/loop-agent.js +98 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/cli/catalog.js +259 -0
  11. package/dist/cli/help.js +55 -0
  12. package/dist/cli/index.js +3 -0
  13. package/dist/cli/program.js +505 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/closeout.js +13 -0
  16. package/dist/commands/coverage-audit.js +14 -0
  17. package/dist/commands/cursor-prompt.js +222 -0
  18. package/dist/commands/cursor-worker.js +43 -0
  19. package/dist/commands/dag-approve.js +102 -0
  20. package/dist/commands/dag-final-verification.js +76 -0
  21. package/dist/commands/dag-init-hybrid.js +56 -0
  22. package/dist/commands/dag-reconcile-tasks.js +51 -0
  23. package/dist/commands/dag-reject.js +91 -0
  24. package/dist/commands/dag-report.js +177 -0
  25. package/dist/commands/dag-resume.js +34 -0
  26. package/dist/commands/dag-run-task.js +470 -0
  27. package/dist/commands/dag-validate.js +186 -0
  28. package/dist/commands/dag-workflow-compile.js +91 -0
  29. package/dist/commands/dag-workflow-plan.js +130 -0
  30. package/dist/commands/dag-workflow-validate.js +66 -0
  31. package/dist/commands/delegate.js +132 -0
  32. package/dist/commands/docs-archive.js +5 -0
  33. package/dist/commands/docs-audit.js +5 -0
  34. package/dist/commands/doctor.js +50 -0
  35. package/dist/commands/goal.js +92 -0
  36. package/dist/commands/handoff-check.js +5 -0
  37. package/dist/commands/harvest.js +44 -0
  38. package/dist/commands/inspect.js +11 -0
  39. package/dist/commands/instructions.js +195 -0
  40. package/dist/commands/knowledge.js +64 -0
  41. package/dist/commands/loop-benchmark.js +72 -0
  42. package/dist/commands/loop.js +241 -0
  43. package/dist/commands/new-task.js +5 -0
  44. package/dist/commands/pi-prompt.js +181 -0
  45. package/dist/commands/pi-reuse-benchmark.js +153 -0
  46. package/dist/commands/plan-list.js +5 -0
  47. package/dist/commands/promote-run.js +29 -0
  48. package/dist/commands/reference-index.js +16 -0
  49. package/dist/commands/run-dag.js +184 -0
  50. package/dist/commands/spine.js +38 -0
  51. package/dist/commands/stats.js +84 -0
  52. package/dist/commands/status.js +56 -0
  53. package/dist/commands/study-init.js +192 -0
  54. package/dist/commands/workflow.js +259 -0
  55. package/dist/commands/worktree-create.js +31 -0
  56. package/dist/commands/worktree-list.js +5 -0
  57. package/dist/commands/worktree-remove.js +26 -0
  58. package/dist/cursor-worker-entry.js +8 -0
  59. package/dist/executors/config-core.js +55 -0
  60. package/dist/executors/config.js +2 -0
  61. package/dist/executors/cursor-artifacts.js +33 -0
  62. package/dist/executors/cursor-execution-log.js +81 -0
  63. package/dist/executors/cursor-executor-artifacts.js +135 -0
  64. package/dist/executors/cursor-executor.js +468 -0
  65. package/dist/executors/cursor-run.js +115 -0
  66. package/dist/executors/cursor-tool.js +94 -0
  67. package/dist/executors/cursor-worker-client.js +213 -0
  68. package/dist/executors/cursor-worker-protocol.js +18 -0
  69. package/dist/executors/cursor-worker-server.js +54 -0
  70. package/dist/executors/cursor-worker.js +3 -0
  71. package/dist/executors/cursor.js +6 -0
  72. package/dist/executors/dag-cursor-executor.js +88 -0
  73. package/dist/executors/dag-pi-executor.js +322 -0
  74. package/dist/executors/dag-static-executor.js +45 -0
  75. package/dist/executors/dag.js +4 -0
  76. package/dist/executors/index.js +8 -0
  77. package/dist/executors/model-routing.js +60 -0
  78. package/dist/executors/pi-event-serializer.js +43 -0
  79. package/dist/executors/pi-executor.js +606 -0
  80. package/dist/executors/pi-reuse-benchmark.js +316 -0
  81. package/dist/executors/pi-runtime-reuse.js +29 -0
  82. package/dist/executors/pi-sdk-executor.js +255 -0
  83. package/dist/executors/pi-sdk.js +1 -0
  84. package/dist/executors/pi.js +3 -0
  85. package/dist/executors/shell-executor.js +300 -0
  86. package/dist/executors/shell-presets.js +47 -0
  87. package/dist/executors/shell-verification.js +251 -0
  88. package/dist/executors/shell-write-guard.js +126 -0
  89. package/dist/executors/shell.js +3 -0
  90. package/dist/executors/static.js +1 -0
  91. package/dist/governance/checks.js +434 -0
  92. package/dist/governance/harness.js +9 -0
  93. package/dist/governance/index.js +3 -0
  94. package/dist/governance/manifest-types.js +128 -0
  95. package/dist/governance/manifest.js +2 -0
  96. package/dist/governance/path-guard.js +69 -0
  97. package/dist/governance/path-guards.js +2 -0
  98. package/dist/governance/profiles.js +3 -0
  99. package/dist/governance/requirement-coverage.js +425 -0
  100. package/dist/governance/skill-safety.js +135 -0
  101. package/dist/governance/spine-audit.js +152 -0
  102. package/dist/records/closeout.js +2 -0
  103. package/dist/records/harvest.js +236 -0
  104. package/dist/records/index.js +3 -0
  105. package/dist/records/one-shot-runs.js +421 -0
  106. package/dist/records/promotion.js +199 -0
  107. package/dist/shared/artifacts-core.js +88 -0
  108. package/dist/shared/artifacts.js +2 -0
  109. package/dist/shared/context-files.js +32 -0
  110. package/dist/shared/context.js +2 -0
  111. package/dist/shared/copy-dir.js +17 -0
  112. package/dist/shared/git-progress.js +165 -0
  113. package/dist/shared/index.js +5 -0
  114. package/dist/shared/logger.js +23 -0
  115. package/dist/shared/one-shot-prompt-args.js +98 -0
  116. package/dist/shared/path-refs.js +31 -0
  117. package/dist/shared/prompts.js +26 -0
  118. package/dist/shared/reference-context.js +238 -0
  119. package/dist/shared/timeout-policy.js +19 -0
  120. package/dist/shared/timeout.js +1 -0
  121. package/dist/shared/types.js +5 -0
  122. package/dist/task/config-types.js +97 -0
  123. package/dist/task/config.js +2 -0
  124. package/dist/task/delegate.js +220 -0
  125. package/dist/task/goal-audit.js +51 -0
  126. package/dist/task/goal-policy.js +8 -0
  127. package/dist/task/goal.js +3 -0
  128. package/dist/task/ids.js +1 -0
  129. package/dist/task/index.js +9 -0
  130. package/dist/task/lifecycle.js +1 -0
  131. package/dist/task/paths.js +1 -0
  132. package/dist/task/read-model.js +149 -0
  133. package/dist/task/runtime.js +699 -0
  134. package/dist/task/source-state.js +1 -0
  135. package/dist/task/state.js +55 -0
  136. package/dist/task/subagent-guidance.js +1 -0
  137. package/dist/task/workflow-state-types.js +92 -0
  138. package/dist/task/worktree-cleanup.js +140 -0
  139. package/dist/task/worktree.js +171 -0
  140. package/dist/workflows/dag/authoring.js +8 -0
  141. package/dist/workflows/dag/authority-surface.js +138 -0
  142. package/dist/workflows/dag/canvas-observer.js +474 -0
  143. package/dist/workflows/dag/decision-envelope.js +502 -0
  144. package/dist/workflows/dag/decision-evidence.js +153 -0
  145. package/dist/workflows/dag/decision-gates.js +1 -0
  146. package/dist/workflows/dag/executor-registry.js +25 -0
  147. package/dist/workflows/dag/facts.js +4 -0
  148. package/dist/workflows/dag/failure-category.js +111 -0
  149. package/dist/workflows/dag/final-verification.js +180 -0
  150. package/dist/workflows/dag/governance-constants.js +5 -0
  151. package/dist/workflows/dag/governance-profile.js +405 -0
  152. package/dist/workflows/dag/index.js +6 -0
  153. package/dist/workflows/dag/init-hybrid.js +855 -0
  154. package/dist/workflows/dag/knowledge-curator.js +162 -0
  155. package/dist/workflows/dag/lifecycle.js +484 -0
  156. package/dist/workflows/dag/prompt-source.js +88 -0
  157. package/dist/workflows/dag/prompt.js +130 -0
  158. package/dist/workflows/dag/reconcile-tasks.js +404 -0
  159. package/dist/workflows/dag/recovery-recommendation.js +226 -0
  160. package/dist/workflows/dag/repair-artifact.js +136 -0
  161. package/dist/workflows/dag/report.js +1019 -0
  162. package/dist/workflows/dag/runner.js +1677 -0
  163. package/dist/workflows/dag/runtime.js +5 -0
  164. package/dist/workflows/dag/skill-instructions.js +471 -0
  165. package/dist/workflows/dag/skills.js +41 -0
  166. package/dist/workflows/dag/spec.js +3 -0
  167. package/dist/workflows/dag/topo.js +30 -0
  168. package/dist/workflows/dag/types.js +275 -0
  169. package/dist/workflows/dag/upstream-artifacts.js +95 -0
  170. package/dist/workflows/dag/validate.js +527 -0
  171. package/dist/workflows/dynamic/artifacts.js +65 -0
  172. package/dist/workflows/dynamic/compile.js +360 -0
  173. package/dist/workflows/dynamic/compileTypes.js +1 -0
  174. package/dist/workflows/dynamic/errors.js +5 -0
  175. package/dist/workflows/dynamic/index.js +7 -0
  176. package/dist/workflows/dynamic/profiles.js +156 -0
  177. package/dist/workflows/dynamic/spec.js +114 -0
  178. package/dist/workflows/dynamic/validate.js +275 -0
  179. package/dist/workflows/loop/actions.js +1334 -0
  180. package/dist/workflows/loop/benchmark.js +510 -0
  181. package/dist/workflows/loop/closeout.js +134 -0
  182. package/dist/workflows/loop/context.js +48 -0
  183. package/dist/workflows/loop/events.js +25 -0
  184. package/dist/workflows/loop/hash.js +32 -0
  185. package/dist/workflows/loop/index.js +8 -0
  186. package/dist/workflows/loop/paths.js +17 -0
  187. package/dist/workflows/loop/rounds.js +81 -0
  188. package/dist/workflows/loop/signals.js +55 -0
  189. package/dist/workflows/loop/state.js +116 -0
  190. package/dist/workflows/loop/templates.js +54 -0
  191. package/dist/workflows/loop/types.js +28 -0
  192. package/docs/README.md +62 -0
  193. package/docs/agent-dag-recovery-playbook.md +158 -0
  194. package/docs/agent-dag-runner.md +40 -0
  195. package/docs/cursor-executor-usage.md +25 -0
  196. package/docs/decisions/README.md +3 -0
  197. package/docs/design/README.md +36 -0
  198. package/docs/development-principles.md +71 -0
  199. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
  200. package/docs/exec-plans/README.md +6 -0
  201. package/docs/exec-plans/active/README.md +5 -0
  202. package/docs/exec-plans/completed/README.md +5 -0
  203. package/docs/feature-workflow.md +184 -0
  204. package/docs/harness-methodology-debugging.md +153 -0
  205. package/docs/harness-methodology-tdd.md +130 -0
  206. package/docs/harness-methodology-verification.md +27 -0
  207. package/docs/loop-agent-harness.md +42 -0
  208. package/docs/progress/README.md +3 -0
  209. package/docs/reports/README.md +3 -0
  210. package/docs/templates/adr.md +60 -0
  211. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
  212. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
  213. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
  214. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
  215. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
  216. package/docs/templates/agent-dag-report.schema.json +423 -0
  217. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
  218. package/docs/templates/agent-dag.base.json +195 -0
  219. package/docs/templates/agent-dag.final-verification.json +190 -0
  220. package/docs/templates/agent-dag.schema.json +316 -0
  221. package/docs/templates/agent-dag.supervised-implementation.json +500 -0
  222. package/docs/templates/exec-plan.md +64 -0
  223. package/docs/templates/feature-spec.md +53 -0
  224. package/docs/templates/hybrid-dag.json +193 -0
  225. package/docs/templates/progress-log.md +17 -0
  226. package/docs/templates/project-start-checklist.md +9 -0
  227. package/docs/templates/qa-report.md +42 -0
  228. package/docs/templates/sprint-contract.md +29 -0
  229. package/docs/verification-matrix.md +30 -0
  230. package/examples/decision-gate-agent-dag.json +123 -0
  231. package/examples/example-dag.json +51 -0
  232. package/examples/hybrid-loop-agent-dag.json +194 -0
  233. package/harness.json +92 -0
  234. package/package.json +61 -0
  235. package/skills/ai-engineering-context/SKILL.md +48 -0
  236. package/skills/loop-agent/SKILL.md +260 -0
  237. package/skills/loop-agent/references/README.md +63 -0
  238. package/skills/loop-agent/references/command-reference.md +315 -0
  239. package/skills/loop-agent/references/harness-policy.md +258 -0
  240. package/skills/loop-agent/references/hybrid-dag.md +216 -0
  241. package/skills/loop-agent/references/learned/README.md +21 -0
  242. package/skills/loop-agent/references/model-routing.md +36 -0
  243. package/skills/loop-agent/references/multi-worktree.md +54 -0
  244. package/skills/loop-agent/references/one-shot-runs.md +85 -0
  245. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
  246. package/skills/loop-agent/references/pi-prompt.md +23 -0
  247. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
  248. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
  249. package/skills/loop-agent/references/task-workflow.md +84 -0
  250. package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
  251. package/skills/requesting-code-review/SKILL.md +101 -0
  252. package/skills/requesting-code-review/code-reviewer.md +168 -0
  253. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  254. package/skills/systematic-debugging/SKILL.md +296 -0
  255. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  256. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  257. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  258. package/skills/systematic-debugging/find-polluter.sh +63 -0
  259. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  260. package/skills/systematic-debugging/test-academic.md +14 -0
  261. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  262. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  263. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  264. package/skills/verification-before-completion/SKILL.md +154 -0
@@ -0,0 +1,300 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { truncateOutput } from "./cursor-executor.js";
6
+ import { expandShellPreset, buildVerdictGateShellCommand } from "./shell-presets.js";
7
+ import { pathsChangedDuringRun, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
8
+ import { buildShellProcessEnv } from "./shell-verification.js";
9
+ const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
10
+ const SUMMARY_STDOUT_MAX = 4_000;
11
+ const SUMMARY_STDERR_MAX = 2_000;
12
+ const NODE_STDOUT_MAX = 12_000;
13
+ const NODE_STDERR_MAX = 8_000;
14
+ function resolveBashExecutable() {
15
+ if (process.env.LOOP_AGENT_BASH)
16
+ return process.env.LOOP_AGENT_BASH;
17
+ if (process.platform !== "win32")
18
+ return "bash";
19
+ 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";
32
+ }
33
+ function isWithinRoot(root, candidate) {
34
+ const relative = path.relative(root, candidate);
35
+ return (relative === "" ||
36
+ (!relative.startsWith("..") && !path.isAbsolute(relative)));
37
+ }
38
+ export function resolveShellCwd(root, requestedCwd) {
39
+ const resolved = requestedCwd ? path.resolve(root, requestedCwd) : root;
40
+ if (!isWithinRoot(root, resolved)) {
41
+ throw new Error(`shell cwd must stay within DAG cwd: ${requestedCwd}`);
42
+ }
43
+ return resolved;
44
+ }
45
+ /** Preset commands first, then explicit shell.commands, then verdictGate (deterministic ordering). */
46
+ export function resolveShellCommands(shell) {
47
+ const fromPreset = shell.preset ? expandShellPreset(shell.preset) : [];
48
+ const explicit = (shell.commands ?? []).filter((command) => command.trim().length > 0);
49
+ const fromVerdictGate = shell.verdictGate
50
+ ? [buildVerdictGateShellCommand(shell.verdictGate)]
51
+ : [];
52
+ return [...fromPreset, ...explicit, ...fromVerdictGate];
53
+ }
54
+ async function readGitStatusPorcelain(cwd) {
55
+ return new Promise((resolve, reject) => {
56
+ const child = spawn("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
57
+ cwd,
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ });
60
+ let stdout = "";
61
+ let stderr = "";
62
+ child.stdout.setEncoding("utf-8");
63
+ child.stderr.setEncoding("utf-8");
64
+ child.stdout.on("data", (chunk) => {
65
+ stdout += chunk;
66
+ });
67
+ child.stderr.on("data", (chunk) => {
68
+ stderr += chunk;
69
+ });
70
+ child.on("error", reject);
71
+ child.on("close", (code) => {
72
+ if (code === 0) {
73
+ resolve(stdout);
74
+ return;
75
+ }
76
+ reject(new Error(`git status failed: ${stderr.trim() || stdout.trim()}`));
77
+ });
78
+ });
79
+ }
80
+ export async function executeShellCommand(input) {
81
+ return new Promise((resolve) => {
82
+ const startedAt = Date.now();
83
+ const injectedEnv = input.dagRunMeta
84
+ ? {
85
+ HARNESS_DAG_RUN_DIR: input.dagRunMeta.runDir,
86
+ HARNESS_DAG_RUN_ID: input.dagRunMeta.runId,
87
+ }
88
+ : undefined;
89
+ const child = spawn(resolveBashExecutable(), ["-lc", input.command], {
90
+ cwd: input.cwd,
91
+ env: buildShellProcessEnv(input.envAllowlist, injectedEnv),
92
+ stdio: ["ignore", "pipe", "pipe"],
93
+ });
94
+ let stdout = "";
95
+ let stderr = "";
96
+ let timedOut = false;
97
+ let settled = false;
98
+ let timeoutHandle;
99
+ let sigkillHandle;
100
+ function finish(result) {
101
+ if (settled)
102
+ return;
103
+ settled = true;
104
+ if (timeoutHandle)
105
+ clearTimeout(timeoutHandle);
106
+ if (sigkillHandle)
107
+ clearTimeout(sigkillHandle);
108
+ resolve({
109
+ command: input.command,
110
+ cwd: input.cwd,
111
+ durationMs: Date.now() - startedAt,
112
+ stdout,
113
+ stderr,
114
+ timedOut,
115
+ ...result,
116
+ });
117
+ }
118
+ child.stdout.setEncoding("utf-8");
119
+ child.stderr.setEncoding("utf-8");
120
+ child.stdout.on("data", (chunk) => {
121
+ stdout += chunk;
122
+ });
123
+ child.stderr.on("data", (chunk) => {
124
+ stderr += chunk;
125
+ });
126
+ child.on("error", (error) => {
127
+ stderr += stderr ? `\n${error.message}` : error.message;
128
+ finish({
129
+ exitCode: null,
130
+ failureCategory: "spawn-error",
131
+ ok: false,
132
+ });
133
+ });
134
+ child.on("close", (code) => {
135
+ finish({
136
+ exitCode: code,
137
+ failureCategory: timedOut
138
+ ? "timeout"
139
+ : code === 0
140
+ ? "success"
141
+ : "nonzero-exit",
142
+ ok: code === 0 && !timedOut,
143
+ });
144
+ });
145
+ if (input.timeoutMs > 0) {
146
+ timeoutHandle = setTimeout(() => {
147
+ timedOut = true;
148
+ child.kill("SIGTERM");
149
+ sigkillHandle = setTimeout(() => {
150
+ if (child.exitCode === null) {
151
+ child.kill("SIGKILL");
152
+ }
153
+ }, 5_000);
154
+ }, input.timeoutMs);
155
+ }
156
+ });
157
+ }
158
+ function summarizeCommandResults(results) {
159
+ const stdout = results
160
+ .map((result) => [`$ ${result.command}`, result.stdout.trim()].filter(Boolean).join("\n"))
161
+ .join("\n\n");
162
+ const stderr = results
163
+ .filter((result) => result.stderr.trim().length > 0)
164
+ .map((result) => [`$ ${result.command}`, result.stderr.trim()].join("\n"))
165
+ .join("\n\n");
166
+ return {
167
+ stdout: truncateOutput(stdout, NODE_STDOUT_MAX).text,
168
+ stderr: truncateOutput(stderr, NODE_STDERR_MAX).text,
169
+ };
170
+ }
171
+ export function buildShellResultSummaryMarkdown(input) {
172
+ const lines = [
173
+ "# Shell execution summary",
174
+ "",
175
+ "## Run metadata",
176
+ "",
177
+ "| Field | Value |",
178
+ "|-------|-------|",
179
+ `| dagRunId | ${input.runId} |`,
180
+ `| dagNodeId | ${input.nodeId} |`,
181
+ `| cwd | ${input.rootCwd} |`,
182
+ `| commandCount | ${input.results.length} |`,
183
+ "",
184
+ "## Commands",
185
+ "",
186
+ "| # | ok | exitCode | failureCategory | durationMs | cwd | command |",
187
+ "|---|----|----------|-----------------|------------|-----|---------|",
188
+ ];
189
+ input.results.forEach((result, index) => {
190
+ lines.push(`| ${index + 1} | ${result.ok} | ${result.exitCode ?? "null"} | ${result.failureCategory} | ${result.durationMs} | ${result.cwd} | ${result.command.replace(/\|/g, "\\|")} |`);
191
+ });
192
+ if (input.writeGuardViolations && input.writeGuardViolations.length > 0) {
193
+ lines.push("", "## Write guard violations", "");
194
+ for (const violation of input.writeGuardViolations) {
195
+ lines.push(`- ${violation}`);
196
+ }
197
+ }
198
+ for (const [index, result] of input.results.entries()) {
199
+ const stdout = truncateOutput(result.stdout, SUMMARY_STDOUT_MAX);
200
+ const stderr = truncateOutput(result.stderr, SUMMARY_STDERR_MAX);
201
+ lines.push("", `## Command ${index + 1}: ${result.command}`, "", "### stdout", "", "```text", stdout.text, "```");
202
+ if (stdout.truncated)
203
+ lines.push("", "_(stdout truncated)_");
204
+ if (stderr.text.trim()) {
205
+ lines.push("", "### stderr", "", "```text", stderr.text, "```");
206
+ if (stderr.truncated)
207
+ lines.push("", "_(stderr truncated)_");
208
+ }
209
+ }
210
+ return `${lines.join("\n")}\n`;
211
+ }
212
+ async function runShellWriteGuard(input) {
213
+ const afterStatus = await readGitStatusPorcelain(input.rootCwd);
214
+ const beforeSnapshot = snapshotGitStatusPorcelain(input.beforeStatus);
215
+ const afterSnapshot = snapshotGitStatusPorcelain(afterStatus);
216
+ const changedPaths = pathsChangedDuringRun(beforeSnapshot, afterSnapshot);
217
+ return validateShellWriteGuard({
218
+ changedPaths,
219
+ writePolicy: input.task.writePolicy,
220
+ writeSet: input.task.writeSet,
221
+ allowedPaths: input.task.allowedPaths,
222
+ forbiddenPaths: input.task.forbiddenPaths,
223
+ });
224
+ }
225
+ export async function executeDagShellNode(input, meta) {
226
+ const shell = input.task.shell;
227
+ const commands = shell ? resolveShellCommands(shell) : [];
228
+ if (!shell || commands.length === 0) {
229
+ throw new Error(`shell task ${input.task.id} requires shell.preset, shell.verdictGate, and/or non-empty shell.commands`);
230
+ }
231
+ const cwd = resolveShellCwd(input.cwd, shell.cwd);
232
+ const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
233
+ const results = [];
234
+ const started = Date.now();
235
+ let beforeStatus;
236
+ try {
237
+ beforeStatus = await readGitStatusPorcelain(input.cwd);
238
+ }
239
+ catch {
240
+ // Non-git cwd: skip write guard (documented limitation).
241
+ beforeStatus = undefined;
242
+ }
243
+ for (const command of commands) {
244
+ const result = await executeShellCommand({
245
+ command,
246
+ cwd,
247
+ timeoutMs,
248
+ envAllowlist: shell.envAllowlist,
249
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
250
+ });
251
+ results.push(result);
252
+ if (!result.ok)
253
+ break;
254
+ }
255
+ let writeGuardViolations = [];
256
+ let writeGuardOk = true;
257
+ if (beforeStatus !== undefined) {
258
+ try {
259
+ const guard = await runShellWriteGuard({
260
+ rootCwd: input.cwd,
261
+ task: input.task,
262
+ beforeStatus,
263
+ });
264
+ writeGuardOk = guard.ok;
265
+ writeGuardViolations = guard.violations;
266
+ }
267
+ catch {
268
+ writeGuardOk = false;
269
+ writeGuardViolations = ["write guard failed: git status unavailable"];
270
+ }
271
+ }
272
+ const commandsOk = results.every((result) => result.ok);
273
+ const ok = commandsOk && writeGuardOk;
274
+ 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({
278
+ nodeId: input.task.id,
279
+ runId: meta.runId,
280
+ rootCwd: input.cwd,
281
+ results,
282
+ writeGuardViolations: writeGuardOk ? undefined : writeGuardViolations,
283
+ }), "utf-8");
284
+ const aggregate = summarizeCommandResults(results);
285
+ const stderrParts = [aggregate.stderr];
286
+ if (!writeGuardOk) {
287
+ stderrParts.push(`write guard failed: ${writeGuardViolations.join(", ")}`);
288
+ }
289
+ return {
290
+ ok,
291
+ stdout: aggregate.stdout,
292
+ stderr: stderrParts.filter(Boolean).join("\n\n"),
293
+ failureCategory: !commandsOk
294
+ ? (firstFailure?.failureCategory ?? "nonzero-exit")
295
+ : !writeGuardOk
296
+ ? "write-guard"
297
+ : "success",
298
+ durationMs: Date.now() - started,
299
+ };
300
+ }
@@ -0,0 +1,47 @@
1
+ /** Preset command lists run before explicit `shell.commands` (deterministic ordering). */
2
+ export const DAG_SHELL_PRESETS = {
3
+ "loop-agent-standard-verify": [
4
+ "npm run lint",
5
+ "npm run typecheck",
6
+ "npx vitest run test/dag-shell-executor.test.ts test/dag-validate.test.ts test/dag-runner.test.ts --reporter=dot",
7
+ ],
8
+ };
9
+ export function expandShellPreset(preset) {
10
+ return [...DAG_SHELL_PRESETS[preset]];
11
+ }
12
+ function escapeShellSingleQuoted(value) {
13
+ return value.replace(/'/g, "'\\''");
14
+ }
15
+ const VERDICT_GATE_NODE_PARSER_FIRST_NON_EMPTY = 'node -e \'const fs=require("fs"); const raw=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const text=raw.assistantText ?? raw.stdout ?? ""; const line=String(text).split(/\\r?\\n/).find((value) => value.trim().length > 0) ?? ""; process.stdout.write(line);\' "${FILE}" || true';
16
+ const VERDICT_GATE_NODE_PARSER_FIRST_VERDICT_LINE = 'node -e \'const fs=require("fs"); const raw=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const text=raw.assistantText ?? raw.stdout ?? ""; const normalize=(value) => { const trimmed=String(value).trim(); const emphasized=trimmed.match(/^\\*{1,3}\\s*(VERDICT:[^*]+?)\\s*\\*{1,3}$/); return (emphasized ? emphasized[1] : trimmed).trim(); }; const line=String(text).split(/\\r?\\n/).map(normalize).find((value) => /^VERDICT:/.test(value)) ?? ""; process.stdout.write(line);\' "${FILE}" || true';
17
+ function verdictGateNodeParser(lineMode) {
18
+ return lineMode === "first-verdict-line"
19
+ ? VERDICT_GATE_NODE_PARSER_FIRST_VERDICT_LINE
20
+ : VERDICT_GATE_NODE_PARSER_FIRST_NON_EMPTY;
21
+ }
22
+ /**
23
+ * Build a single shell command that reads `$HARNESS_DAG_RUN_DIR/<fromNodeId>.json`
24
+ * (injected by the shell executor from runtime meta), parses a single candidate
25
+ * verdict line from `assistantText ?? stdout`, and accepts only exact verdict values.
26
+ * Modes:
27
+ * - first-non-empty: first non-empty line
28
+ * - first-verdict-line: first line matching /^VERDICT:/ after trim and optional whole-line Markdown emphasis normalization
29
+ *
30
+ * Must stay one command so `shell.commands[]` process isolation cannot drop state variables.
31
+ */
32
+ export function buildVerdictGateShellCommand(gate) {
33
+ const gateLabel = gate.label ?? `${gate.fromNodeId} verdict`;
34
+ const jsonFile = `${gate.fromNodeId}.json`;
35
+ const lineMode = gate.lineMode ?? "first-non-empty";
36
+ const parser = verdictGateNodeParser(lineMode);
37
+ const preamble = `test -n "\${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for ${gateLabel} gate" >&2; exit 1; }; FILE="\${HARNESS_DAG_RUN_DIR}/${jsonFile}"; test -f "\${FILE}" || { echo "missing ${gateLabel} JSON output: \${FILE}" >&2; exit 1; }; FIRST=$(${parser})`;
38
+ const blockedMessage = `${gateLabel} gate blocked: \${FIRST:-missing VERDICT line}`;
39
+ if (gate.accept.length === 1) {
40
+ const expected = escapeShellSingleQuoted(gate.accept[0]);
41
+ return `${preamble}; test "\${FIRST}" = '${expected}' || { echo "${blockedMessage}" >&2; exit 1; }`;
42
+ }
43
+ const acceptPattern = gate.accept
44
+ .map((value) => `'${escapeShellSingleQuoted(value)}'`)
45
+ .join("|");
46
+ return `${preamble}; case "\${FIRST}" in ${acceptPattern}) exit 0 ;; *) echo "${blockedMessage}" >&2; exit 1 ;; esac`;
47
+ }
@@ -0,0 +1,251 @@
1
+ import { spawn } from "node:child_process";
2
+ export const DEFAULT_VERIFY_TIMEOUT_MS = 1_800_000;
3
+ const VERIFY_ENV_ALLOWLIST = [
4
+ "HOME",
5
+ "PATH",
6
+ "USER",
7
+ "LOGNAME",
8
+ "SHELL",
9
+ "TMPDIR",
10
+ "TEMP",
11
+ "TMP",
12
+ "LANG",
13
+ "LC_ALL",
14
+ "LC_CTYPE",
15
+ "TERM",
16
+ "CI",
17
+ "NODE_OPTIONS",
18
+ "NODE_PATH",
19
+ "PNPM_HOME",
20
+ "COREPACK_ENABLE",
21
+ "COREPACK_ENABLE_AUTO_PIN",
22
+ "COREPACK_HOME",
23
+ ];
24
+ export function buildVerifyProcessEnv(mode = "clean") {
25
+ if (mode === "inherit") {
26
+ return { ...process.env };
27
+ }
28
+ const env = { CI: "true" };
29
+ for (const key of VERIFY_ENV_ALLOWLIST) {
30
+ const value = process.env[key];
31
+ if (value !== undefined && value !== "") {
32
+ env[key] = value;
33
+ }
34
+ }
35
+ return env;
36
+ }
37
+ /** Clean verify env plus explicitly allowlisted host variables (no broad passthrough). */
38
+ export function buildShellProcessEnv(envAllowlist, injected) {
39
+ const env = buildVerifyProcessEnv("clean");
40
+ if (envAllowlist) {
41
+ for (const key of envAllowlist) {
42
+ const value = process.env[key];
43
+ if (value !== undefined) {
44
+ env[key] = value;
45
+ }
46
+ }
47
+ }
48
+ if (injected) {
49
+ for (const [key, value] of Object.entries(injected)) {
50
+ env[key] = value;
51
+ }
52
+ }
53
+ return env;
54
+ }
55
+ export function extractLastFailureCategory(verifyReportText) {
56
+ const categoryLine = verifyReportText.match(/- category:\s*([a-z0-9-]+)/i);
57
+ if (categoryLine?.[1])
58
+ return categoryLine[1].toLowerCase();
59
+ const kindLine = verifyReportText.match(/:\s*(environment-failure|governance-failure|typecheck-failure|test-failure|verify-failure|timeout)/i);
60
+ return kindLine?.[1]?.toLowerCase();
61
+ }
62
+ export function renderVerifyReport(results) {
63
+ const passed = results.filter((result) => result.ok);
64
+ const failed = results.filter((result) => !result.ok);
65
+ const commandBlock = results
66
+ .map((result) => result.args.join(" "))
67
+ .join("\n");
68
+ const passedList = passed.length > 0
69
+ ? passed.map((result) => `- ${result.label}`).join("\n")
70
+ : "- 无";
71
+ const failedList = failed.length > 0
72
+ ? failed
73
+ .map((result) => `- ${result.label}${result.timedOut ? " (TIMEOUT)" : ""}`)
74
+ .join("\n")
75
+ : "- 无";
76
+ const failureKinds = failed.length > 0
77
+ ? failed
78
+ .map((result) => `- ${result.label}: ${result.failureCategory ?? "unknown"}${result.attempts > 1 ? ` (attempts=${result.attempts})` : ""}`)
79
+ .join("\n")
80
+ : "- 无";
81
+ const failureSummary = failed.length > 0
82
+ ? failed
83
+ .map((result) => `- ${result.label}${result.timedOut ? " [TIMEOUT]" : ""}: ${summarize(result.stderr || result.stdout)}`)
84
+ .join("\n")
85
+ : "- 无";
86
+ const commandDetails = results
87
+ .map((result) => [
88
+ `### ${result.label}`,
89
+ `- command: \`${result.args.join(" ")}\``,
90
+ `- cwd: \`${result.cwd}\``,
91
+ `- exitCode: ${result.exitCode ?? "null"}`,
92
+ `- attempts: ${result.attempts}`,
93
+ `- category: ${result.failureCategory ?? (result.ok ? "success" : "unknown")}`,
94
+ `- durationMs: ${result.durationMs}`,
95
+ ].join("\n"))
96
+ .join("\n\n");
97
+ const nextStep = failed.length > 0
98
+ ? "- [ ] 进入修复子循环\n- [ ] 需要人工判断失败原因是否由环境导致"
99
+ : "- [x] 无需继续修复\n- [ ] 可进入 retrospective";
100
+ return [
101
+ "# 验证结果",
102
+ "",
103
+ "## 1. 执行的命令",
104
+ "```bash",
105
+ commandBlock,
106
+ "```",
107
+ "",
108
+ "## 2. 通过项",
109
+ passedList,
110
+ "",
111
+ "## 3. 失败项",
112
+ failedList,
113
+ "",
114
+ "## 4. 失败摘要",
115
+ failureSummary,
116
+ "",
117
+ "## 5. 失败分类",
118
+ failureKinds,
119
+ "",
120
+ "## 6. 命令详情",
121
+ commandDetails,
122
+ "",
123
+ "## 7. 下一步建议",
124
+ nextStep,
125
+ "",
126
+ ].join("\n");
127
+ }
128
+ export async function executeCommand(command, defaultTimeoutMs = DEFAULT_VERIFY_TIMEOUT_MS) {
129
+ return new Promise((resolve, reject) => {
130
+ const [binary, ...args] = command.args;
131
+ const timeoutMs = command.timeoutMs ?? defaultTimeoutMs;
132
+ const startedAt = Date.now();
133
+ const child = spawn(binary, args, {
134
+ cwd: command.cwd,
135
+ env: command.env ?? process.env,
136
+ stdio: ["ignore", "pipe", "pipe"],
137
+ });
138
+ let stdout = "";
139
+ let stderr = "";
140
+ let timedOut = false;
141
+ let timeoutHandle;
142
+ let sigkillHandle;
143
+ child.stdout.setEncoding("utf-8");
144
+ child.stderr.setEncoding("utf-8");
145
+ child.stdout.on("data", (chunk) => {
146
+ stdout += chunk;
147
+ });
148
+ child.stderr.on("data", (chunk) => {
149
+ stderr += chunk;
150
+ });
151
+ child.on("error", (error) => {
152
+ if (timeoutHandle)
153
+ clearTimeout(timeoutHandle);
154
+ if (sigkillHandle)
155
+ clearTimeout(sigkillHandle);
156
+ reject(error);
157
+ });
158
+ child.on("close", (code) => {
159
+ if (timeoutHandle)
160
+ clearTimeout(timeoutHandle);
161
+ if (sigkillHandle)
162
+ clearTimeout(sigkillHandle);
163
+ resolve({
164
+ ...command,
165
+ attempts: 1,
166
+ durationMs: Date.now() - startedAt,
167
+ exitCode: code,
168
+ failureCategory: classifyVerifyCommandFailure(command, code, timedOut, stderr, stdout),
169
+ ok: code === 0,
170
+ stderr,
171
+ stdout,
172
+ timedOut,
173
+ });
174
+ });
175
+ if (timeoutMs > 0) {
176
+ timeoutHandle = setTimeout(() => {
177
+ timedOut = true;
178
+ child.kill("SIGTERM");
179
+ sigkillHandle = setTimeout(() => {
180
+ if (child.exitCode === null) {
181
+ child.kill("SIGKILL");
182
+ }
183
+ }, 5_000);
184
+ }, timeoutMs);
185
+ }
186
+ });
187
+ }
188
+ export async function executeVerifyCommands(commands, defaultTimeoutMs, options) {
189
+ if (options.mode === "parallel") {
190
+ return Promise.all(commands.map((command) => executeCommandWithRetry(command, defaultTimeoutMs, options.retryCount)));
191
+ }
192
+ const results = [];
193
+ for (const command of commands) {
194
+ const result = await executeCommandWithRetry(command, defaultTimeoutMs, options.retryCount);
195
+ results.push(result);
196
+ if (options.failFast && !result.ok) {
197
+ break;
198
+ }
199
+ }
200
+ return results;
201
+ }
202
+ async function executeCommandWithRetry(command, defaultTimeoutMs, retryCount) {
203
+ let lastResult = await executeCommand(command, defaultTimeoutMs);
204
+ let attempts = 1;
205
+ while (!lastResult.ok && attempts <= retryCount) {
206
+ attempts += 1;
207
+ lastResult = await executeCommand(command, defaultTimeoutMs);
208
+ }
209
+ return {
210
+ ...lastResult,
211
+ attempts,
212
+ };
213
+ }
214
+ export function summarizeVerifyFailure(results) {
215
+ const firstFailure = results.find((result) => !result.ok);
216
+ if (!firstFailure)
217
+ return "success";
218
+ return firstFailure.failureCategory ?? "verify-failure";
219
+ }
220
+ function classifyVerifyCommandFailure(command, exitCode, timedOut, stderr, stdout) {
221
+ if (timedOut)
222
+ return "timeout";
223
+ if (exitCode === 0)
224
+ return "success";
225
+ const joined = `${command.label} ${command.args.join(" ")} ${stderr} ${stdout}`.toLowerCase();
226
+ if (joined.includes("check-repo") || joined.includes("governance"))
227
+ return "governance-failure";
228
+ if (/pnpm.*configured to use|corepack invoked pnpm|packagemanager|cannot find pnpm|enoent.*pnpm/.test(joined)) {
229
+ return "environment-failure";
230
+ }
231
+ if (joined.includes("typecheck") || joined.includes("tsc"))
232
+ return "typecheck-failure";
233
+ if (joined.includes("test"))
234
+ return "test-failure";
235
+ if (/not found|enoent/.test(joined))
236
+ return "environment-failure";
237
+ return "verify-failure";
238
+ }
239
+ function summarize(text) {
240
+ return text.replace(/\s+/g, " ").trim().slice(0, 240) || "no output captured";
241
+ }
242
+ export function resolveVerifyPreset(preset, taskConfig) {
243
+ if (!preset || preset === "auto") {
244
+ if (taskConfig.complexity === "small")
245
+ return "quick";
246
+ if (taskConfig.complexity === "large")
247
+ return "full";
248
+ return "standard";
249
+ }
250
+ return preset;
251
+ }