@tea-agent/loop-agent 0.3.0 → 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 (119) hide show
  1. package/AGENTS.md +135 -133
  2. package/CHANGELOG.md +88 -63
  3. package/README.md +171 -168
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/init.js +457 -457
  7. package/dist/commands/loop-benchmark.js +11 -11
  8. package/dist/commands/pi-reuse-benchmark.js +16 -16
  9. package/dist/executors/cursor-executor.js +1 -1
  10. package/dist/executors/dag-pi-executor.js +8 -1
  11. package/dist/task/runtime.js +27 -27
  12. package/dist/worker/cli.js +119 -0
  13. package/dist/worker/loop-agent/command-result.js +1 -0
  14. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  15. package/dist/worker/loop-agent/parse-json.js +14 -0
  16. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  17. package/dist/worker/pool/failure-routing.js +98 -0
  18. package/dist/worker/pool/run-store.js +117 -0
  19. package/dist/worker/pool/types.js +1 -0
  20. package/dist/worker/preflight.js +108 -0
  21. package/dist/worker/profile-mapping.js +76 -0
  22. package/dist/worker/progress-reporter.js +81 -0
  23. package/dist/worker/report/morning-report.js +69 -0
  24. package/dist/worker/repos/repo-resolver.js +23 -0
  25. package/dist/worker/run-task/run-task.js +359 -0
  26. package/dist/worker/runner/run-ready.js +216 -0
  27. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  28. package/dist/worker/task-graph/ready-queue.js +23 -0
  29. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  30. package/dist/worker/task-graph/types.js +1 -0
  31. package/dist/worker/task-graph/validate.js +188 -0
  32. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  33. package/dist/worker/task-spec/schema.js +116 -0
  34. package/dist/worker/task-spec/types.js +1 -0
  35. package/dist/worker/task-spec/validate.js +352 -0
  36. package/dist/workflows/dag/canvas-observer.js +275 -275
  37. package/docs/README.md +65 -61
  38. package/docs/agent-dag-recovery-playbook.md +184 -184
  39. package/docs/agent-dag-runner.md +42 -42
  40. package/docs/architecture/runtime-boundaries.md +147 -147
  41. package/docs/cursor-executor-usage.md +25 -25
  42. package/docs/decisions/README.md +3 -3
  43. package/docs/design/README.md +36 -36
  44. package/docs/development-principles.md +73 -71
  45. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  46. package/docs/exec-plans/README.md +6 -6
  47. package/docs/exec-plans/active/README.md +7 -7
  48. package/docs/exec-plans/completed/README.md +19 -11
  49. package/docs/feature-workflow.md +186 -186
  50. package/docs/harness-methodology-debugging.md +153 -153
  51. package/docs/harness-methodology-tdd.md +130 -130
  52. package/docs/harness-methodology-verification.md +27 -27
  53. package/docs/loop-agent-harness.md +42 -42
  54. package/docs/production-readiness.md +96 -96
  55. package/docs/progress/README.md +3 -3
  56. package/docs/reports/README.md +5 -5
  57. package/docs/skills/README.md +6 -6
  58. package/docs/skills/vetted-skill-registry.md +22 -22
  59. package/docs/templates/adr.md +60 -60
  60. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  61. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  62. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  63. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  64. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  65. package/docs/templates/agent-dag-report.schema.json +454 -454
  66. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  67. package/docs/templates/agent-dag.base.json +195 -195
  68. package/docs/templates/agent-dag.final-verification.json +190 -190
  69. package/docs/templates/agent-dag.schema.json +316 -316
  70. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  71. package/docs/templates/exec-plan.md +64 -64
  72. package/docs/templates/feature-spec.md +53 -53
  73. package/docs/templates/hybrid-dag.json +193 -193
  74. package/docs/templates/production-readiness-checklist.md +57 -57
  75. package/docs/templates/progress-log.md +17 -17
  76. package/docs/templates/project-start-checklist.md +9 -9
  77. package/docs/templates/qa-report.md +48 -48
  78. package/docs/templates/sprint-contract.md +29 -29
  79. package/docs/verification-matrix.md +41 -41
  80. package/examples/decision-gate-agent-dag.json +123 -123
  81. package/examples/example-dag.json +51 -51
  82. package/examples/hybrid-loop-agent-dag.json +194 -194
  83. package/harness.json +89 -89
  84. package/package.json +60 -58
  85. package/skills/ai-engineering-context/SKILL.md +48 -48
  86. package/skills/code-review-core/SKILL.md +20 -20
  87. package/skills/codebase-scout/SKILL.md +19 -19
  88. package/skills/loop-agent/SKILL.md +147 -145
  89. package/skills/loop-agent/references/README.md +67 -67
  90. package/skills/loop-agent/references/command-reference.md +368 -340
  91. package/skills/loop-agent/references/harness-policy.md +259 -258
  92. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  93. package/skills/loop-agent/references/learned/README.md +21 -21
  94. package/skills/loop-agent/references/long-running-loop.md +59 -59
  95. package/skills/loop-agent/references/model-routing.md +36 -36
  96. package/skills/loop-agent/references/multi-worktree.md +54 -54
  97. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  98. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  99. package/skills/loop-agent/references/pi-prompt.md +23 -23
  100. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  101. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  102. package/skills/loop-agent/references/task-workflow.md +84 -84
  103. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  104. package/skills/requesting-code-review/SKILL.md +101 -101
  105. package/skills/requesting-code-review/code-reviewer.md +168 -168
  106. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  107. package/skills/systematic-debugging/SKILL.md +296 -296
  108. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  109. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  110. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  111. package/skills/systematic-debugging/find-polluter.sh +63 -63
  112. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  113. package/skills/systematic-debugging/test-academic.md +14 -14
  114. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  115. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  116. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  117. package/skills/test-driven-development/SKILL.md +20 -20
  118. package/skills/verification-before-completion/SKILL.md +154 -154
  119. 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:`,
@@ -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
+ }