@tea-agent/loop-agent 0.2.1 → 0.4.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 (135) hide show
  1. package/AGENTS.md +91 -87
  2. package/CHANGELOG.md +89 -52
  3. package/README.md +195 -180
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/args.js +6 -0
  7. package/dist/application/dag/generate-task-dag.js +2 -0
  8. package/dist/application/dag/run-dag.js +3 -0
  9. package/dist/application/dag/validate-dag.js +40 -0
  10. package/dist/cli/command-definitions.js +2 -2
  11. package/dist/cli/program.js +24 -4
  12. package/dist/commands/init.js +1011 -459
  13. package/dist/commands/loop-benchmark.js +11 -11
  14. package/dist/commands/pi-reuse-benchmark.js +16 -16
  15. package/dist/executors/cursor-executor.js +1 -1
  16. package/dist/executors/dag-pi-executor.js +8 -1
  17. package/dist/task/runtime.js +27 -27
  18. package/dist/worker/cli.js +119 -0
  19. package/dist/worker/loop-agent/command-result.js +1 -0
  20. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  21. package/dist/worker/loop-agent/parse-json.js +14 -0
  22. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  23. package/dist/worker/pool/failure-routing.js +98 -0
  24. package/dist/worker/pool/run-store.js +117 -0
  25. package/dist/worker/pool/types.js +1 -0
  26. package/dist/worker/preflight.js +108 -0
  27. package/dist/worker/profile-mapping.js +76 -0
  28. package/dist/worker/progress-reporter.js +81 -0
  29. package/dist/worker/report/morning-report.js +69 -0
  30. package/dist/worker/repos/repo-resolver.js +23 -0
  31. package/dist/worker/run-task/run-task.js +359 -0
  32. package/dist/worker/runner/run-ready.js +216 -0
  33. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  34. package/dist/worker/task-graph/ready-queue.js +23 -0
  35. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  36. package/dist/worker/task-graph/types.js +1 -0
  37. package/dist/worker/task-graph/validate.js +188 -0
  38. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  39. package/dist/worker/task-spec/schema.js +116 -0
  40. package/dist/worker/task-spec/types.js +1 -0
  41. package/dist/worker/task-spec/validate.js +352 -0
  42. package/dist/workflows/dag/canvas-observer.js +275 -275
  43. package/dist/workflows/dag/dynamic-runtime/loop-until.js +2 -1
  44. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  45. package/dist/workflows/dag/init-hybrid.js +3 -3
  46. package/dist/workflows/dag/skills.js +3 -3
  47. package/dist/workflows/dag/types.js +2 -0
  48. package/dist/workflows/dynamic/compile.js +11 -0
  49. package/dist/workflows/dynamic/spec.js +1 -0
  50. package/docs/README.md +72 -65
  51. package/docs/agent-dag-recovery-playbook.md +184 -184
  52. package/docs/agent-dag-runner.md +42 -40
  53. package/docs/architecture/runtime-boundaries.md +147 -147
  54. package/docs/cursor-executor-usage.md +25 -25
  55. package/docs/decisions/README.md +3 -3
  56. package/docs/design/README.md +36 -36
  57. package/docs/development-principles.md +73 -71
  58. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  59. package/docs/exec-plans/README.md +6 -6
  60. package/docs/exec-plans/active/README.md +7 -10
  61. package/docs/exec-plans/completed/README.md +19 -9
  62. package/docs/feature-workflow.md +186 -186
  63. package/docs/harness-methodology-debugging.md +153 -153
  64. package/docs/harness-methodology-tdd.md +130 -130
  65. package/docs/harness-methodology-verification.md +27 -27
  66. package/docs/init-surface.manifest.json +175 -0
  67. package/docs/loop-agent-harness.md +42 -42
  68. package/docs/production-readiness.md +96 -96
  69. package/docs/progress/README.md +3 -3
  70. package/docs/reports/README.md +5 -5
  71. package/docs/skills/README.md +6 -0
  72. package/docs/skills/vetted-skill-registry.md +26 -0
  73. package/docs/templates/adr.md +60 -60
  74. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  75. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  76. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  77. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  78. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  79. package/docs/templates/agent-dag-report.schema.json +454 -454
  80. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  81. package/docs/templates/agent-dag.base.json +195 -195
  82. package/docs/templates/agent-dag.final-verification.json +190 -190
  83. package/docs/templates/agent-dag.schema.json +316 -316
  84. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  85. package/docs/templates/exec-plan.md +64 -64
  86. package/docs/templates/feature-spec.md +53 -53
  87. package/docs/templates/hybrid-dag.json +193 -193
  88. package/docs/templates/init-evolution-review.md +33 -0
  89. package/docs/templates/production-readiness-checklist.md +57 -57
  90. package/docs/templates/progress-log.md +17 -17
  91. package/docs/templates/project-start-checklist.md +9 -9
  92. package/docs/templates/qa-report.md +48 -48
  93. package/docs/templates/sprint-contract.md +29 -29
  94. package/docs/verification-matrix.md +41 -41
  95. package/examples/decision-gate-agent-dag.json +123 -123
  96. package/examples/example-dag.json +51 -51
  97. package/examples/hybrid-loop-agent-dag.json +194 -194
  98. package/harness.json +94 -92
  99. package/package.json +66 -62
  100. package/skills/ai-engineering-context/SKILL.md +48 -48
  101. package/skills/code-review-core/SKILL.md +20 -0
  102. package/skills/codebase-scout/SKILL.md +19 -0
  103. package/skills/init-capability-evolution/SKILL.md +69 -0
  104. package/skills/loop-agent/SKILL.md +147 -145
  105. package/skills/loop-agent/references/README.md +67 -67
  106. package/skills/loop-agent/references/command-reference.md +403 -357
  107. package/skills/loop-agent/references/harness-policy.md +259 -258
  108. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  109. package/skills/loop-agent/references/learned/README.md +21 -21
  110. package/skills/loop-agent/references/long-running-loop.md +59 -59
  111. package/skills/loop-agent/references/model-routing.md +36 -36
  112. package/skills/loop-agent/references/multi-worktree.md +54 -54
  113. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  114. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  115. package/skills/loop-agent/references/pi-prompt.md +23 -23
  116. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  117. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  118. package/skills/loop-agent/references/task-workflow.md +84 -84
  119. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  120. package/skills/requesting-code-review/SKILL.md +101 -101
  121. package/skills/requesting-code-review/code-reviewer.md +168 -168
  122. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  123. package/skills/systematic-debugging/SKILL.md +296 -296
  124. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  125. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  126. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  127. package/skills/systematic-debugging/find-polluter.sh +63 -63
  128. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  129. package/skills/systematic-debugging/test-academic.md +14 -14
  130. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  131. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  132. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  133. package/skills/test-driven-development/SKILL.md +20 -0
  134. package/skills/verification-before-completion/SKILL.md +154 -154
  135. package/skills/webapp-testing/SKILL.md +19 -0
@@ -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:`,
@@ -148,7 +148,14 @@ export function buildDagPiUserMessage(task, persona, step) {
148
148
  ].join(" ");
149
149
  }
150
150
  function resolveDagPiModelConfig(model) {
151
- return { provider: DAG_PI_MODEL_PROVIDERS[model] ?? DEFAULT_DAG_PI_PROVIDER, model };
151
+ const provider = DAG_PI_MODEL_PROVIDERS[model] ?? DEFAULT_DAG_PI_PROVIDER;
152
+ return {
153
+ provider,
154
+ model,
155
+ ...(provider === "wizard-local" && model === "gpt-5.5"
156
+ ? { thinking: "low" }
157
+ : {}),
158
+ };
152
159
  }
153
160
  const SUMMARY_STDOUT_MAX = 4_000;
154
161
  const SUMMARY_STDERR_MAX = 2_000;
@@ -236,35 +236,35 @@ export function shouldInjectSubagentGuidance(step, mode) {
236
236
  return false;
237
237
  }
238
238
  /** Advisory guidance for `analyze-plan` mode. */
239
- export const SUBAGENT_GUIDANCE_STANDARD = `<subagent_guidance>
240
- You have access to the \`subagent\` tool for lightweight delegation within this step.
241
- Use it only for read-only tasks:
242
- - Parallel scout: dispatch multiple subagents to search/read different areas simultaneously
243
- - Chain: scout -> planner (one subagent scouts, another plans based on findings)
244
- - Reviewer: have a subagent review your analysis/plan before finalizing
245
- Do NOT use subagent for writing, editing, or executing commands.
246
- Subagent output is advisory only; always verify and incorporate findings into your own output.
247
- Do NOT treat subagent results as authoritative state or artifact sources.
239
+ export const SUBAGENT_GUIDANCE_STANDARD = `<subagent_guidance>
240
+ You have access to the \`subagent\` tool for lightweight delegation within this step.
241
+ Use it only for read-only tasks:
242
+ - Parallel scout: dispatch multiple subagents to search/read different areas simultaneously
243
+ - Chain: scout -> planner (one subagent scouts, another plans based on findings)
244
+ - Reviewer: have a subagent review your analysis/plan before finalizing
245
+ Do NOT use subagent for writing, editing, or executing commands.
246
+ Subagent output is advisory only; always verify and incorporate findings into your own output.
247
+ Do NOT treat subagent results as authoritative state or artifact sources.
248
248
  </subagent_guidance>`;
249
249
  /** Strong guidance for `full` mode — prescriptive when to delegate. */
250
- export const SUBAGENT_GUIDANCE_STRONG = `<subagent_guidance>
251
- You have access to the \`subagent\` tool for lightweight delegation within this step.
252
-
253
- You SHOULD delegate to subagent scouts when:
254
- - The task requires scanning 3+ directories or comparing implementations across modules
255
- - You would otherwise need 5+ sequential read/grep calls to gather context
256
- - A reviewer subagent can independently catch scope drift before you finalize your output
257
-
258
- Delegation saves context tokens and produces better results.
259
-
260
- Allowed patterns:
261
- - Parallel scout: dispatch 2-3 subagents simultaneously to cover different file trees
262
- - Chain: scout -> planner (one subagent scouts, another plans based on findings)
263
- - Reviewer: have a subagent review your analysis/plan before finalizing
264
-
265
- Do NOT use subagent for writing, editing, or executing commands.
266
- Subagent output is advisory only; always verify and incorporate findings into your own output.
267
- Do NOT treat subagent results as authoritative state or artifact sources.
250
+ export const SUBAGENT_GUIDANCE_STRONG = `<subagent_guidance>
251
+ You have access to the \`subagent\` tool for lightweight delegation within this step.
252
+
253
+ You SHOULD delegate to subagent scouts when:
254
+ - The task requires scanning 3+ directories or comparing implementations across modules
255
+ - You would otherwise need 5+ sequential read/grep calls to gather context
256
+ - A reviewer subagent can independently catch scope drift before you finalize your output
257
+
258
+ Delegation saves context tokens and produces better results.
259
+
260
+ Allowed patterns:
261
+ - Parallel scout: dispatch 2-3 subagents simultaneously to cover different file trees
262
+ - Chain: scout -> planner (one subagent scouts, another plans based on findings)
263
+ - Reviewer: have a subagent review your analysis/plan before finalizing
264
+
265
+ Do NOT use subagent for writing, editing, or executing commands.
266
+ Subagent output is advisory only; always verify and incorporate findings into your own output.
267
+ Do NOT treat subagent results as authoritative state or artifact sources.
268
268
  </subagent_guidance>`;
269
269
  /** Preserved for backward compatibility (alias of STANDARD). */
270
270
  export const SUBAGENT_GUIDANCE = SUBAGENT_GUIDANCE_STANDARD;
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ import YAML from "yaml";
7
+ import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
8
+ import { resolveLoopAgentProfile } from "./profile-mapping.js";
9
+ import { writeMorningReport } from "./report/morning-report.js";
10
+ import { buildBatchRunId, runReadyTasks } from "./runner/run-ready.js";
11
+ import { createProgressReporter } from "./progress-reporter.js";
12
+ import { taskSpecSchema } from "./task-spec/schema.js";
13
+ import { validateTaskSpec } from "./task-spec/validate.js";
14
+ export function buildAgentWorkerProgram() {
15
+ const program = new Command();
16
+ program
17
+ .name("agent-worker")
18
+ .description("Local product-line worker utilities for TaskSpec validation")
19
+ .showHelpAfterError()
20
+ .showSuggestionAfterError();
21
+ const task = program.command("task").description("TaskSpec utilities");
22
+ const batch = program.command("batch").description("Task Pool batch utilities");
23
+ const report = program.command("report").description("Task Pool reporting utilities");
24
+ task
25
+ .command("validate")
26
+ .argument("<task-yaml>", "TaskSpec YAML file")
27
+ .description("Validate a TaskSpec and print JSON")
28
+ .action(async (taskYaml) => {
29
+ const taskSpecPath = path.resolve(taskYaml);
30
+ const raw = await readTaskYaml(taskSpecPath);
31
+ const result = await validateTaskSpec(raw, { taskSpecPath });
32
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
33
+ if (!result.ok)
34
+ process.exitCode = 1;
35
+ });
36
+ task
37
+ .command("explain-profile")
38
+ .argument("<task-yaml>", "TaskSpec YAML file")
39
+ .description("Explain business profile to loop-agent profile mapping")
40
+ .action(async (taskYaml) => {
41
+ const raw = await readTaskYaml(path.resolve(taskYaml));
42
+ const parsed = taskSpecSchema.parse(raw);
43
+ const result = resolveLoopAgentProfile(parsed);
44
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
45
+ });
46
+ batch
47
+ .command("run-ready")
48
+ .requiredOption("--feature-dir <dir>", "Feature directory containing tasks/task-graph.yaml")
49
+ .requiredOption("--repo <repo-root>", "Target repo root")
50
+ .option("--limit <count>", "Maximum ready tasks to run")
51
+ .option("--loop-agent-bin <bin>", "loop-agent binary", "loop-agent")
52
+ .option("--batch-run-id <id>", "Batch run id")
53
+ .option("--check-repo", "Run bash scripts/check-repo.sh during target repo preflight")
54
+ .option("--check-repo-command <command...>", "Override the check-repo preflight command")
55
+ .option("--quiet", "Suppress human-readable progress on stderr (JSON still goes to stdout)")
56
+ .option("--pi-model <model>", "Smoke override: force every pi executor node to this model (drops --strict-models)")
57
+ .description("Run ready TaskSpec tasks serially")
58
+ .action(async (options) => {
59
+ const repoRoot = path.resolve(options.repo);
60
+ const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
61
+ const client = new LoopAgentClient({
62
+ loopAgentBin: options.loopAgentBin,
63
+ artifactRoot: path.join(repoRoot, ".task-pool", "artifacts", batchRunId),
64
+ });
65
+ const progress = createProgressReporter({ quiet: options.quiet ?? false });
66
+ const result = await runReadyTasks({
67
+ repoRoot,
68
+ featureDir: path.resolve(options.featureDir),
69
+ ...(options.limit ? { limit: Number.parseInt(options.limit, 10) } : {}),
70
+ batchRunId,
71
+ client,
72
+ runCheckRepo: options.checkRepo ?? false,
73
+ ...(options.checkRepoCommand
74
+ ? { checkRepoCommand: options.checkRepoCommand }
75
+ : {}),
76
+ progress,
77
+ ...(options.piModel ? { piModel: options.piModel } : {}),
78
+ });
79
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
80
+ if (result.status !== "completed")
81
+ process.exitCode = 1;
82
+ });
83
+ report
84
+ .command("morning")
85
+ .requiredOption("--repo <repo-root>", "Target repo root")
86
+ .option("--batch-run-id <id>", "Filter to one batch run id")
87
+ .option("--output <path>", "Write markdown report to this path")
88
+ .description("Render a markdown morning report from Task Pool runs")
89
+ .action(async (options) => {
90
+ const repoRoot = path.resolve(options.repo);
91
+ const outputPath = options.output ??
92
+ path.join(repoRoot, ".task-pool", "reports", "morning-report.md");
93
+ await writeMorningReport({
94
+ repoRoot,
95
+ ...(options.batchRunId ? { batchRunId: options.batchRunId } : {}),
96
+ outputPath: path.resolve(outputPath),
97
+ });
98
+ process.stdout.write(`${JSON.stringify({ ok: true, outputPath }, null, 2)}\n`);
99
+ });
100
+ return program;
101
+ }
102
+ export async function main(argv = process.argv) {
103
+ await buildAgentWorkerProgram().parseAsync(argv);
104
+ }
105
+ async function readTaskYaml(taskSpecPath) {
106
+ return YAML.parse(await readFile(taskSpecPath, "utf-8"));
107
+ }
108
+ function isDirectRun() {
109
+ const entry = process.argv[1];
110
+ if (!entry)
111
+ return false;
112
+ return path.resolve(entry) === fileURLToPath(import.meta.url);
113
+ }
114
+ if (isDirectRun()) {
115
+ main().catch((error) => {
116
+ console.error(error instanceof Error ? error.message : String(error));
117
+ process.exit(1);
118
+ });
119
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parseCommandJson } from "./parse-json.js";
5
+ export const DEFAULT_WORKER_COMMAND_TIMEOUT_MS = 120_000;
6
+ export class LoopAgentClient {
7
+ loopAgentBin;
8
+ baseArgs;
9
+ artifactRoot;
10
+ defaultTimeoutMs;
11
+ env;
12
+ constructor(options) {
13
+ this.loopAgentBin = options.loopAgentBin;
14
+ this.baseArgs = options.baseArgs ?? [];
15
+ this.artifactRoot = options.artifactRoot;
16
+ this.defaultTimeoutMs =
17
+ options.defaultTimeoutMs ?? DEFAULT_WORKER_COMMAND_TIMEOUT_MS;
18
+ this.env = options.env;
19
+ }
20
+ async run(args, options) {
21
+ return this.runCommand(this.loopAgentBin, [...this.baseArgs, ...args], args, options);
22
+ }
23
+ async runExternal(command, args, options) {
24
+ return this.runCommand(command, args, args, options);
25
+ }
26
+ async runCommand(command, commandArgs, resultArgs, options) {
27
+ const startedAt = Date.now();
28
+ const artifactDir = path.join(this.artifactRoot, sanitizeName(options.artifactName));
29
+ await mkdir(artifactDir, { recursive: true });
30
+ const { stdout, stderr, exitCode, timedOut } = await spawnCommand({
31
+ command,
32
+ args: commandArgs,
33
+ cwd: options.cwd,
34
+ timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
35
+ env: { ...process.env, ...this.env, ...options.env },
36
+ });
37
+ const result = {
38
+ ok: exitCode === 0 && !timedOut,
39
+ args: resultArgs,
40
+ command,
41
+ cwd: options.cwd,
42
+ durationMs: Date.now() - startedAt,
43
+ exitCode,
44
+ stdout,
45
+ stderr,
46
+ timedOut,
47
+ artifacts: {
48
+ dir: artifactDir,
49
+ stdoutPath: path.join(artifactDir, "stdout.txt"),
50
+ stderrPath: path.join(artifactDir, "stderr.txt"),
51
+ resultPath: path.join(artifactDir, "result.json"),
52
+ },
53
+ };
54
+ if (options.expectJson) {
55
+ const parsed = parseCommandJson(stdout);
56
+ if (parsed.ok) {
57
+ result.json = parsed.value;
58
+ }
59
+ else {
60
+ result.ok = false;
61
+ result.parseFailure = parsed.failure;
62
+ }
63
+ }
64
+ await writeFile(result.artifacts.stdoutPath, stdout, "utf-8");
65
+ await writeFile(result.artifacts.stderrPath, stderr, "utf-8");
66
+ await writeFile(result.artifacts.resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
67
+ return result;
68
+ }
69
+ }
70
+ function spawnCommand(input) {
71
+ return new Promise((resolve, reject) => {
72
+ const child = spawn(input.command, input.args, {
73
+ cwd: input.cwd,
74
+ env: input.env,
75
+ shell: false,
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ });
78
+ let stdout = "";
79
+ let stderr = "";
80
+ let timedOut = false;
81
+ const timeout = setTimeout(() => {
82
+ timedOut = true;
83
+ child.kill("SIGTERM");
84
+ }, input.timeoutMs);
85
+ child.stdout.setEncoding("utf8");
86
+ child.stderr.setEncoding("utf8");
87
+ child.stdout.on("data", (chunk) => {
88
+ stdout += chunk;
89
+ });
90
+ child.stderr.on("data", (chunk) => {
91
+ stderr += chunk;
92
+ });
93
+ child.on("error", (error) => {
94
+ clearTimeout(timeout);
95
+ reject(error);
96
+ });
97
+ child.on("close", (exitCode) => {
98
+ clearTimeout(timeout);
99
+ resolve({ stdout, stderr, exitCode, timedOut });
100
+ });
101
+ });
102
+ }
103
+ function sanitizeName(name) {
104
+ return name.replace(/[^a-zA-Z0-9._-]+/g, "-");
105
+ }
@@ -0,0 +1,14 @@
1
+ export function parseCommandJson(stdout) {
2
+ try {
3
+ return { ok: true, value: JSON.parse(stdout) };
4
+ }
5
+ catch (error) {
6
+ return {
7
+ ok: false,
8
+ failure: {
9
+ code: "json-parse-failed",
10
+ message: error instanceof Error ? error.message : String(error),
11
+ },
12
+ };
13
+ }
14
+ }
@@ -0,0 +1,157 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { writeTaskConfig, writeTaskArtifactFile } from "../../infrastructure/harness/task-store.js";
5
+ import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
6
+ import { resolveLoopAgentProfile } from "../profile-mapping.js";
7
+ import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
8
+ import { validateTaskSpec } from "../task-spec/validate.js";
9
+ export async function materializeTaskSpec(options) {
10
+ const now = options.now ?? new Date();
11
+ const validation = await validateTaskSpec(options.taskSpec, {
12
+ taskSpecPath: options.taskSpecPath,
13
+ });
14
+ if (!validation.ok) {
15
+ const codes = validation.errors.map((error) => error.code).join(", ");
16
+ throw new Error(`TaskSpec validation failed: ${codes}`);
17
+ }
18
+ const harnessTaskId = buildHarnessTaskId(options.taskSpec, now);
19
+ const existingConfig = await loadExistingTaskConfig(options.repoRoot, harnessTaskId);
20
+ if (!existingConfig) {
21
+ const newTaskResult = await options.client.run(["new-task", harnessTaskId, options.taskSpec.title], {
22
+ cwd: options.repoRoot,
23
+ artifactName: `new-task-${harnessTaskId}`,
24
+ });
25
+ if (!newTaskResult.ok) {
26
+ const racedConfig = await loadExistingTaskConfig(options.repoRoot, harnessTaskId);
27
+ if (!racedConfig) {
28
+ throw new Error(`loop-agent new-task failed with exit ${newTaskResult.exitCode}: ${newTaskResult.stderr}`);
29
+ }
30
+ }
31
+ }
32
+ const paths = getTaskPaths(options.repoRoot, harnessTaskId);
33
+ const baseConfig = await loadTaskConfig(options.repoRoot, harnessTaskId);
34
+ const taskConfig = {
35
+ ...baseConfig,
36
+ taskId: harnessTaskId,
37
+ title: options.taskSpec.title,
38
+ allowedPaths: options.taskSpec.constraints.allowed_paths,
39
+ forbiddenPaths: options.taskSpec.constraints.forbidden_paths,
40
+ hardConstraints: options.taskSpec.constraints.hard_constraints,
41
+ complexity: mapRiskLevelToComplexity(options.taskSpec.risk_level),
42
+ verifyMode: options.taskSpec.verify.mode,
43
+ verifyPreset: options.taskSpec.verify.preset,
44
+ verifyQuota: options.taskSpec.verify.quota,
45
+ autoCommitAfterVerify: false,
46
+ };
47
+ await writeTaskConfig(options.repoRoot, harnessTaskId, taskConfig);
48
+ const sourcePaths = {
49
+ requirementPath: path.join(paths.sourceDir, "需求.md"),
50
+ constraintsPath: path.join(paths.sourceDir, "执行约束.md"),
51
+ taskYamlPath: path.join(paths.sourceDir, "task.yaml"),
52
+ };
53
+ await mkdir(paths.sourceDir, { recursive: true });
54
+ await writeFile(sourcePaths.requirementPath, renderRequirementMarkdown(options.taskSpec), "utf-8");
55
+ await writeFile(sourcePaths.constraintsPath, renderConstraintsMarkdown(options.taskSpec), "utf-8");
56
+ await writeFile(sourcePaths.taskYamlPath, YAML.stringify(options.taskSpec), "utf-8");
57
+ const profileMapping = resolveLoopAgentProfile(options.taskSpec);
58
+ const manifest = {
59
+ schemaVersion: 1,
60
+ businessId: options.taskSpec.id,
61
+ harnessTaskId,
62
+ featureId: options.taskSpec.feature_id,
63
+ loopAgentProfile: profileMapping.loopAgentProfile,
64
+ taskConfigPath: paths.taskConfigPath,
65
+ source: sourcePaths,
66
+ };
67
+ await writeTaskArtifactFile(options.repoRoot, harnessTaskId, "materialize-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`);
68
+ return manifest;
69
+ }
70
+ export function buildHarnessTaskId(taskSpec, now) {
71
+ const date = now.toISOString().slice(0, 10);
72
+ const slug = slugify(`${taskSpec.id}-${taskSpec.title}`);
73
+ return `${date}-${slug}`;
74
+ }
75
+ function slugify(value) {
76
+ return value
77
+ .normalize("NFKD")
78
+ .replace(/[\u0300-\u036f]/g, "")
79
+ .toLowerCase()
80
+ .replace(/[^a-z0-9]+/g, "-")
81
+ .replace(/^-+|-+$/g, "")
82
+ .replace(/-{2,}/g, "-");
83
+ }
84
+ function renderRequirementMarkdown(taskSpec) {
85
+ const lines = [
86
+ `# ${taskSpec.title}`,
87
+ "",
88
+ `TaskSpec business id: ${taskSpec.id}`,
89
+ `Feature id: ${taskSpec.feature_id}`,
90
+ `Task type: ${taskSpec.type}`,
91
+ `Risk level: ${taskSpec.risk_level}`,
92
+ "",
93
+ ];
94
+ if (taskSpec.description) {
95
+ lines.push("## Description", "", taskSpec.description, "");
96
+ }
97
+ lines.push("## Goals", "", ...bulletLines(taskSpec.scope.goals), "");
98
+ if (taskSpec.scope.non_goals.length > 0) {
99
+ lines.push("## Non Goals", "", ...bulletLines(taskSpec.scope.non_goals), "");
100
+ }
101
+ if (taskSpec.scope.assumptions.length > 0) {
102
+ lines.push("## Assumptions", "", ...bulletLines(taskSpec.scope.assumptions), "");
103
+ }
104
+ if (taskSpec.scope.open_questions.length > 0) {
105
+ lines.push("## Open Questions", "", ...bulletLines(taskSpec.scope.open_questions), "");
106
+ }
107
+ lines.push("## Acceptance References", "", ...bulletLines(taskSpec.acceptance_refs), "");
108
+ lines.push("## Required Outputs", "", ...bulletLines(taskSpec.outputs.required), "");
109
+ return `${lines.join("\n").trimEnd()}\n`;
110
+ }
111
+ function renderConstraintsMarkdown(taskSpec) {
112
+ const lines = [
113
+ `# Execution Constraints for ${taskSpec.id}`,
114
+ "",
115
+ "## Allowed Paths",
116
+ "",
117
+ ...bulletLines(taskSpec.constraints.allowed_paths),
118
+ "",
119
+ "## Forbidden Paths",
120
+ "",
121
+ ...bulletLines(taskSpec.constraints.forbidden_paths),
122
+ "",
123
+ "## Hard Constraints",
124
+ "",
125
+ ...bulletLines(taskSpec.constraints.hard_constraints),
126
+ "",
127
+ "## Verification Commands",
128
+ "",
129
+ ...bulletLines(taskSpec.verify.commands.map((command) => command.command)),
130
+ "",
131
+ "## Source Docs",
132
+ "",
133
+ ...bulletLines(Object.entries(taskSpec.source_docs).map(([key, value]) => `${key}: ${value}`)),
134
+ ];
135
+ return `${lines.join("\n").trimEnd()}\n`;
136
+ }
137
+ function bulletLines(values) {
138
+ if (values.length === 0)
139
+ return ["- None"];
140
+ return values.map((value) => `- ${value}`);
141
+ }
142
+ async function loadExistingTaskConfig(repoRoot, taskId) {
143
+ try {
144
+ return await loadTaskConfig(repoRoot, taskId);
145
+ }
146
+ catch (error) {
147
+ if (isNotFound(error))
148
+ return undefined;
149
+ throw error;
150
+ }
151
+ }
152
+ function isNotFound(error) {
153
+ return Boolean(error &&
154
+ typeof error === "object" &&
155
+ "code" in error &&
156
+ error.code === "ENOENT");
157
+ }
@@ -0,0 +1,98 @@
1
+ const PRODUCT_CATEGORIES = new Set([
2
+ "SpecUnclear",
3
+ "ContractMismatch",
4
+ "ProductBug",
5
+ "TestBug",
6
+ "EnvFailure",
7
+ "FlakyTest",
8
+ "RiskyChange",
9
+ "DependencyFailure",
10
+ "NeedsHuman",
11
+ "Unknown",
12
+ ]);
13
+ const FOLLOW_UP_BY_CATEGORY = {
14
+ SpecUnclear: "spec-clarification",
15
+ ContractMismatch: "architecture-contract-fix",
16
+ ProductBug: "dev-fix",
17
+ TestBug: "qa-fix-test",
18
+ EnvFailure: "env-fix",
19
+ FlakyTest: "flaky-test-analysis",
20
+ RiskyChange: "human-review",
21
+ DependencyFailure: "unblock-dependency",
22
+ NeedsHuman: "human-review",
23
+ Unknown: "human-triage",
24
+ };
25
+ const FOLLOW_UP_PREFIX_BY_CATEGORY = {
26
+ SpecUnclear: "SPEC",
27
+ ContractMismatch: "CONTRACT",
28
+ ProductBug: "FIX",
29
+ TestBug: "QA-FIX",
30
+ EnvFailure: "ENV",
31
+ FlakyTest: "FLAKY",
32
+ RiskyChange: "REVIEW",
33
+ DependencyFailure: "UNBLOCK",
34
+ NeedsHuman: "REVIEW",
35
+ Unknown: "TRIAGE",
36
+ };
37
+ const DAG_TO_PRODUCT_CATEGORY = {
38
+ validation: "SpecUnclear",
39
+ executor: "EnvFailure",
40
+ "write-guard": "RiskyChange",
41
+ timeout: "EnvFailure",
42
+ auth: "EnvFailure",
43
+ "human-required": "NeedsHuman",
44
+ "human-rejected": "NeedsHuman",
45
+ "shell-command": "ProductBug",
46
+ "static-error": "SpecUnclear",
47
+ "decision-envelope": "NeedsHuman",
48
+ skipped: "DependencyFailure",
49
+ unknown: "Unknown",
50
+ };
51
+ export function deriveFailureRoute(result) {
52
+ if (result.status === "succeeded")
53
+ return undefined;
54
+ const primaryFailure = readObject(result.reportDecision.primaryFailure);
55
+ const category = readProductCategory(primaryFailure, "productLineFailureCategory") ??
56
+ readProductCategory(primaryFailure, "productLineCategory") ??
57
+ readProductCategory(primaryFailure, "product_line_failure_category") ??
58
+ mapDagCategory(readString(primaryFailure, "failureCategory")) ??
59
+ mapDagCategory(readString(primaryFailure, "normalizedFailureCategory")) ??
60
+ categoryFromDecisionReason(result.reportDecision.reason);
61
+ const recommendedFollowUpKind = readString(primaryFailure, "recommendedFollowUpKind") ??
62
+ readString(primaryFailure, "recommendedFollowUp") ??
63
+ FOLLOW_UP_BY_CATEGORY[category];
64
+ return {
65
+ category,
66
+ recommendedFollowUpKind,
67
+ derivedFollowUpTaskId: `${FOLLOW_UP_PREFIX_BY_CATEGORY[category]}-${result.businessId}`,
68
+ source: primaryFailure ? "report-primary-failure" : "fallback",
69
+ };
70
+ }
71
+ function readProductCategory(value, key) {
72
+ const candidate = readString(value, key);
73
+ return candidate && PRODUCT_CATEGORIES.has(candidate)
74
+ ? candidate
75
+ : undefined;
76
+ }
77
+ function mapDagCategory(value) {
78
+ if (!value || value === "success")
79
+ return undefined;
80
+ return DAG_TO_PRODUCT_CATEGORY[value] ?? "Unknown";
81
+ }
82
+ function categoryFromDecisionReason(reason) {
83
+ if (reason === "report-json-unavailable" || reason === "report-run-missing") {
84
+ return "Unknown";
85
+ }
86
+ return "Unknown";
87
+ }
88
+ function readObject(value) {
89
+ if (!value || typeof value !== "object" || Array.isArray(value))
90
+ return undefined;
91
+ return value;
92
+ }
93
+ function readString(value, key) {
94
+ if (!value)
95
+ return undefined;
96
+ const child = value[key];
97
+ return typeof child === "string" ? child : undefined;
98
+ }