@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.
- package/AGENTS.md +135 -133
- package/CHANGELOG.md +88 -63
- package/README.md +171 -168
- package/bin/agent-worker.js +22 -0
- package/bin/loop-agent.js +21 -21
- package/dist/commands/init.js +457 -457
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/cursor-executor.js +1 -1
- package/dist/executors/dag-pi-executor.js +8 -1
- package/dist/task/runtime.js +27 -27
- package/dist/worker/cli.js +119 -0
- package/dist/worker/loop-agent/command-result.js +1 -0
- package/dist/worker/loop-agent/loop-agent-client.js +105 -0
- package/dist/worker/loop-agent/parse-json.js +14 -0
- package/dist/worker/materialize/harness-task-materializer.js +157 -0
- package/dist/worker/pool/failure-routing.js +98 -0
- package/dist/worker/pool/run-store.js +117 -0
- package/dist/worker/pool/types.js +1 -0
- package/dist/worker/preflight.js +108 -0
- package/dist/worker/profile-mapping.js +76 -0
- package/dist/worker/progress-reporter.js +81 -0
- package/dist/worker/report/morning-report.js +69 -0
- package/dist/worker/repos/repo-resolver.js +23 -0
- package/dist/worker/run-task/run-task.js +359 -0
- package/dist/worker/runner/run-ready.js +216 -0
- package/dist/worker/task-graph/acceptance-schema.js +25 -0
- package/dist/worker/task-graph/ready-queue.js +23 -0
- package/dist/worker/task-graph/task-graph-schema.js +28 -0
- package/dist/worker/task-graph/types.js +1 -0
- package/dist/worker/task-graph/validate.js +188 -0
- package/dist/worker/task-spec/complexity-mapping.js +8 -0
- package/dist/worker/task-spec/schema.js +116 -0
- package/dist/worker/task-spec/types.js +1 -0
- package/dist/worker/task-spec/validate.js +352 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/docs/README.md +65 -61
- package/docs/agent-dag-recovery-playbook.md +184 -184
- package/docs/agent-dag-runner.md +42 -42
- package/docs/architecture/runtime-boundaries.md +147 -147
- package/docs/cursor-executor-usage.md +25 -25
- package/docs/decisions/README.md +3 -3
- package/docs/design/README.md +36 -36
- package/docs/development-principles.md +73 -71
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +7 -7
- package/docs/exec-plans/completed/README.md +19 -11
- package/docs/feature-workflow.md +186 -186
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/loop-agent-harness.md +42 -42
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +3 -3
- package/docs/reports/README.md +5 -5
- package/docs/skills/README.md +6 -6
- package/docs/skills/vetted-skill-registry.md +22 -22
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +454 -454
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +316 -316
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/verification-matrix.md +41 -41
- package/examples/decision-gate-agent-dag.json +123 -123
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +89 -89
- package/package.json +60 -58
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/loop-agent/SKILL.md +147 -145
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +368 -340
- package/skills/loop-agent/references/harness-policy.md +259 -258
- package/skills/loop-agent/references/hybrid-dag.md +216 -216
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +59 -59
- package/skills/loop-agent/references/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +84 -84
- package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/SKILL.md +296 -296
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/test-driven-development/SKILL.md +20 -20
- package/skills/verification-before-completion/SKILL.md +154 -154
- 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
|
-
|
|
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;
|
package/dist/task/runtime.js
CHANGED
|
@@ -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
|
+
}
|