ai-dev-harness 0.1.0 → 0.1.2

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 (45) hide show
  1. package/COLLEAGUE_TRIAL_GUIDE.md +175 -0
  2. package/ONLINE_INSTALL_GUIDE.md +458 -0
  3. package/README.md +253 -54
  4. package/dist/agent.js +74 -49
  5. package/dist/approval.js +64 -0
  6. package/dist/audit.js +1 -0
  7. package/dist/checkpoint.js +83 -0
  8. package/dist/cli.js +326 -99
  9. package/dist/config.js +118 -9
  10. package/dist/context.js +62 -21
  11. package/dist/contextScore.js +44 -0
  12. package/dist/diagnostics.js +58 -0
  13. package/dist/doctor.js +100 -0
  14. package/dist/feedback.js +5 -0
  15. package/dist/init.js +8 -20
  16. package/dist/integration.js +84 -93
  17. package/dist/loop.js +116 -0
  18. package/dist/mcp.js +29 -112
  19. package/dist/metrics.js +91 -0
  20. package/dist/replay.js +27 -0
  21. package/dist/runPaths.js +31 -0
  22. package/dist/summarize.js +170 -39
  23. package/dist/templates.js +39 -215
  24. package/package.json +5 -2
  25. package/scripts/read-evidence.ps1 +45 -3
  26. package/scripts/run-harness.ps1 +9 -6
  27. package/templates/default-project/AGENTS.md +9 -0
  28. package/templates/default-project/SKILLS.md +13 -0
  29. package/templates/default-project/TOOLS.md +11 -0
  30. package/templates/default-project/skills/add_feature/SKILL.md +34 -0
  31. package/templates/default-project/skills/add_feature/context.yaml +33 -0
  32. package/templates/default-project/skills/add_feature/verify.yaml +2 -0
  33. package/templates/default-project/skills/fix_bug/SKILL.md +45 -0
  34. package/templates/default-project/skills/fix_bug/context.yaml +33 -0
  35. package/templates/default-project/skills/fix_bug/verify.yaml +2 -0
  36. package/templates/default-project/skills/update_docs/SKILL.md +28 -0
  37. package/templates/default-project/skills/update_docs/context.yaml +30 -0
  38. package/templates/default-project/skills/update_docs/verify.yaml +2 -0
  39. package/templates/default-project/skills/write_tests/SKILL.md +30 -0
  40. package/templates/default-project/skills/write_tests/context.yaml +32 -0
  41. package/templates/default-project/skills/write_tests/verify.yaml +2 -0
  42. package/templates/integrations/claude/SKILL.md +115 -0
  43. package/templates/integrations/codex/AGENTS_BLOCK.md +82 -0
  44. package/templates/integrations/cursor/ai-dev-harness.mdc +55 -0
  45. package/templates/integrations/gemini/GEMINI_BLOCK.md +46 -0
@@ -0,0 +1,83 @@
1
+ import { rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { git, runCommand } from "./utils.js";
4
+ function normalizeRel(file) {
5
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
6
+ }
7
+ function statusPath(line) {
8
+ const trimmed = line.trim();
9
+ if (!trimmed)
10
+ return "";
11
+ const rename = trimmed.match(/^R.?\s+(.+)\s+->\s+(.+)$/);
12
+ if (rename)
13
+ return normalizeRel(rename[2]);
14
+ return normalizeRel(trimmed.slice(3).trim());
15
+ }
16
+ function untrackedFiles(status) {
17
+ return status
18
+ .split(/\r?\n/)
19
+ .filter((line) => line.startsWith("?? "))
20
+ .map(statusPath)
21
+ .filter(Boolean);
22
+ }
23
+ function underRoot(file, root) {
24
+ const normalized = normalizeRel(file);
25
+ const normalizedRoot = normalizeRel(root).replace(/\/?$/, "/");
26
+ return normalized === normalizedRoot.slice(0, -1) || normalized.startsWith(normalizedRoot);
27
+ }
28
+ export async function createCheckpoint(options) {
29
+ const head = await git(options.cwd, "rev-parse HEAD");
30
+ const runRel = path.relative(options.cwd, options.runDir).replace(/\\/g, "/");
31
+ const runRoot = runRel.split("/")[0];
32
+ const rawStatusBefore = await git(options.cwd, "status --short");
33
+ const gitStatusBefore = rawStatusBefore
34
+ .split(/\r?\n/)
35
+ .filter((line) => {
36
+ const file = line.trim().slice(3).replace(/\\/g, "/");
37
+ return file && file !== runRoot && file !== `${runRoot}/` && !file.startsWith(`${runRoot}/`) && !file.startsWith(`${runRel}/`);
38
+ })
39
+ .join("\n");
40
+ const preDiff = await git(options.cwd, "diff --no-ext-diff");
41
+ const checkpoint = {
42
+ head,
43
+ baselineClean: !gitStatusBefore,
44
+ gitStatusBefore,
45
+ iteration: options.iteration,
46
+ restoreSupported: !gitStatusBefore && Boolean(head),
47
+ untrackedFilesBefore: untrackedFiles(gitStatusBefore),
48
+ excludedStatusRoots: [runRoot]
49
+ };
50
+ await writeFile(options.preDiffPath, preDiff, "utf8");
51
+ await writeFile(options.checkpointPath, `${JSON.stringify(checkpoint, null, 2)}\n`, "utf8");
52
+ return checkpoint;
53
+ }
54
+ export async function writePostIterationDiff(cwd, postDiffPath) {
55
+ const diff = await git(cwd, "diff --no-ext-diff");
56
+ await writeFile(postDiffPath, diff, "utf8");
57
+ return diff;
58
+ }
59
+ export async function restoreCheckpoint(cwd, checkpoint) {
60
+ if (!checkpoint.restoreSupported || !checkpoint.baselineClean) {
61
+ return {
62
+ ok: false,
63
+ message: "该 checkpoint 不支持自动 restore:运行开始前存在未提交改动,或缺少 Git HEAD。"
64
+ };
65
+ }
66
+ const currentStatus = await git(cwd, "status --short");
67
+ if (!currentStatus)
68
+ return { ok: true, message: "工作区已经是 clean,无需 restore。" };
69
+ const untrackedBefore = new Set((checkpoint.untrackedFilesBefore ?? []).map(normalizeRel));
70
+ const excludedRoots = checkpoint.excludedStatusRoots ?? [];
71
+ const newUntracked = untrackedFiles(currentStatus).filter((file) => !untrackedBefore.has(file) && !excludedRoots.some((root) => underRoot(file, root)));
72
+ const result = await runCommand("git restore --worktree --staged .", cwd, undefined, 60_000);
73
+ if (result.exitCode !== 0)
74
+ return { ok: false, message: result.stderr || "git restore failed" };
75
+ for (const file of newUntracked) {
76
+ const absolute = path.resolve(cwd, file);
77
+ const relative = path.relative(cwd, absolute);
78
+ if (relative.startsWith("..") || path.isAbsolute(relative))
79
+ continue;
80
+ await rm(absolute, { force: true, recursive: true });
81
+ }
82
+ return { ok: true, message: "已通过 git restore 恢复工作区,并清理本轮新增的未跟踪文件。" };
83
+ }
package/dist/cli.js CHANGED
@@ -1,26 +1,38 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, writeFile } from "node:fs/promises";
2
+ import { copyFile, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { AuditLogger, prepareRunFiles, writeJson } from "./audit.js";
5
- import { createAgentAdapter } from "./agent.js";
5
+ import { checkAgentContract, createAgentAdapter } from "./agent.js";
6
+ import { evaluateApprovalGate } from "./approval.js";
7
+ import { createCheckpoint, restoreCheckpoint, writePostIterationDiff } from "./checkpoint.js";
6
8
  import { loadConfig, loadSkill } from "./config.js";
7
9
  import { buildContextPack } from "./context.js";
10
+ import { writeContextScore } from "./contextScore.js";
11
+ import { writeVerifyDiagnostics } from "./diagnostics.js";
12
+ import { runDoctor } from "./doctor.js";
13
+ import { feedbackEvent } from "./feedback.js";
8
14
  import { initHarness } from "./init.js";
9
15
  import { installIntegration } from "./integration.js";
10
- import { makeRunPaths } from "./runPaths.js";
16
+ import { evaluateIteration, renderReworkFeedback, writeLoopArtifacts } from "./loop.js";
17
+ import { collectRunMetrics, renderMetricsText } from "./metrics.js";
18
+ import { buildReplayPlan, renderReplayPlan } from "./replay.js";
19
+ import { makeIterationPaths, makeRunPaths } from "./runPaths.js";
11
20
  import { writeSummary } from "./summarize.js";
12
21
  import { runVerify } from "./verify.js";
13
- import { git, pathExists } from "./utils.js";
22
+ import { ensureDir, git, pathExists } from "./utils.js";
23
+ const booleanArgs = new Set(["dry", "json", "live", "no-integration"]);
14
24
  function parseArgs(argv) {
15
25
  const [command = "help", ...rest] = argv;
16
- const args = {};
26
+ const args = { _: [] };
17
27
  for (let i = 0; i < rest.length; i += 1) {
18
28
  const item = rest[i];
19
- if (!item.startsWith("--"))
29
+ if (!item.startsWith("--")) {
30
+ args._.push(item);
20
31
  continue;
32
+ }
21
33
  const key = item.slice(2);
22
34
  const next = rest[i + 1];
23
- if (!next || next.startsWith("--"))
35
+ if (booleanArgs.has(key) || !next || next.startsWith("--"))
24
36
  args[key] = true;
25
37
  else {
26
38
  args[key] = next;
@@ -32,24 +44,48 @@ function parseArgs(argv) {
32
44
  function usage() {
33
45
  return `AI Dev Harness
34
46
 
35
- Usage:
47
+ 用法:
36
48
  harness init
37
- harness run --task <text> --skill <name> --agent <codex|claude>
38
- harness run --task <text> --skill <name> --agent <codex|claude> --live
49
+ harness init --no-integration
50
+ harness doctor
51
+ harness smoke --agent <codex|claude|cursor|gemini> [--live]
52
+ harness run --task <text> --skill <name> --agent <codex|claude|cursor|gemini>
53
+ harness run --task <text> --skill <name> --agent <codex|claude|cursor|gemini> --live
39
54
  harness verify --run <run_id_or_path>
40
55
  harness summarize --run <run_id_or_path>
41
- harness install-integration --agent <claude|codex|both> [--target <repo>] [--command <harness command>]
56
+ harness restore --run <run_id_or_path> --iteration <n>
57
+ harness runs metrics [--json]
58
+ harness replay --run <run_id_or_path> [--dry] [--agent <codex|claude|cursor|gemini>]
59
+ harness install-integration --agent <claude|codex|cursor|gemini|both|all> [--target <repo>] [--command <harness command>]
42
60
  `;
43
61
  }
62
+ async function commandDoctor(cwd) {
63
+ const config = await loadConfig(cwd);
64
+ const result = await runDoctor(cwd, config);
65
+ console.log(result.text);
66
+ if (result.failed)
67
+ throw new Error("doctor 检查失败,请先修复 FAIL 项。");
68
+ }
69
+ async function commandSmoke(cwd, args) {
70
+ const agentName = requireAgent(requireString(args, "agent"));
71
+ const live = Boolean(args.live);
72
+ await commandRun(cwd, {
73
+ task: "Smoke test only. Analyze repository context and do not modify repository files.",
74
+ skill: "update_docs",
75
+ agent: agentName,
76
+ live
77
+ });
78
+ }
44
79
  function requireString(args, key) {
45
80
  const value = args[key];
46
81
  if (typeof value !== "string" || !value.trim())
47
- throw new Error(`Missing required --${key}`);
82
+ throw new Error(`缺少必填参数 --${key}`);
48
83
  return value;
49
84
  }
50
85
  function requireAgent(value) {
51
- if (value !== "codex" && value !== "claude")
52
- throw new Error(`Invalid agent: ${value}. Expected codex or claude.`);
86
+ if (value !== "codex" && value !== "claude" && value !== "cursor" && value !== "gemini") {
87
+ throw new Error(`无效 agent:${value}。期望 codex、claude、cursor gemini。`);
88
+ }
53
89
  return value;
54
90
  }
55
91
  function quoteCommandPath(filePath) {
@@ -63,17 +99,49 @@ async function resolveRunDir(cwd, runArg) {
63
99
  const underAudit = path.join(cwd, config.audit.path, runArg);
64
100
  if (await pathExists(underAudit))
65
101
  return underAudit;
66
- throw new Error(`Run not found: ${runArg}`);
102
+ throw new Error(`未找到运行记录:${runArg}`);
67
103
  }
68
104
  async function commandInit(cwd) {
69
- const result = await initHarness(cwd);
70
- console.log(`Initialized AI Dev Harness in ${cwd}`);
71
- console.log(`Created ${result.created.length} file(s), skipped ${result.skipped.length} existing file(s).`);
105
+ const packageRoot = path.resolve(path.dirname(process.argv[1]), "..");
106
+ const result = await initHarness(cwd, packageRoot);
107
+ console.log(`AI Dev Harness 初始化完成:${cwd}`);
108
+ console.log(`创建 ${result.created.length} 个文件,跳过 ${result.skipped.length} 个已存在文件。`);
109
+ if (process.argv.includes("--no-integration")) {
110
+ console.log("已跳过 Agent 对话集成。");
111
+ return;
112
+ }
113
+ const harnessCliPath = path.resolve(process.argv[1]);
114
+ const harnessCommand = `node ${quoteCommandPath(harnessCliPath)}`;
115
+ const integration = await installIntegration({ targetDir: cwd, harnessCommand, harnessCliPath, agent: "all", packageRoot });
116
+ console.log("Claude/Codex/Cursor/Gemini 对话集成已安装。");
117
+ console.log(`集成创建:${integration.created.length ? integration.created.join(", ") : "无"}`);
118
+ console.log(`集成更新:${integration.updated.length ? integration.updated.join(", ") : "无"}`);
119
+ console.log(`集成跳过:${integration.skipped.length ? integration.skipped.join(", ") : "无"}`);
72
120
  }
73
121
  function sayLive(live, message) {
74
122
  if (live)
75
123
  console.log(`[harness] ${message}`);
76
124
  }
125
+ async function copyIfExists(from, to) {
126
+ if (await pathExists(from))
127
+ await copyFile(from, to);
128
+ }
129
+ async function syncIterationSnapshot(paths, iterationPaths) {
130
+ await copyIfExists(iterationPaths.contextPack, paths.contextPack);
131
+ await copyIfExists(iterationPaths.agentPrompt, paths.agentPrompt);
132
+ await copyIfExists(iterationPaths.plan, paths.plan);
133
+ await copyIfExists(iterationPaths.executionLog, paths.executionLog);
134
+ await copyIfExists(iterationPaths.agentContract, paths.agentContract);
135
+ await copyIfExists(iterationPaths.gitDiff, paths.gitDiff);
136
+ await copyIfExists(iterationPaths.verify, paths.verify);
137
+ await copyIfExists(iterationPaths.verifyDiagnostics, paths.verifyDiagnostics);
138
+ await copyIfExists(iterationPaths.contextScore, paths.contextScore);
139
+ await copyIfExists(iterationPaths.approvalRequired, paths.approvalRequired);
140
+ await copyIfExists(iterationPaths.feedback, paths.feedback);
141
+ await copyIfExists(iterationPaths.checkpoint, paths.checkpoint);
142
+ await copyIfExists(iterationPaths.preIterationDiff, paths.preIterationDiff);
143
+ await copyIfExists(iterationPaths.postIterationDiff, paths.postIterationDiff);
144
+ }
77
145
  async function commandRun(cwd, args) {
78
146
  const task = requireString(args, "task");
79
147
  const skillName = requireString(args, "skill");
@@ -84,8 +152,8 @@ async function commandRun(cwd, args) {
84
152
  const paths = makeRunPaths(cwd, config, task);
85
153
  await prepareRunFiles(paths);
86
154
  const audit = new AuditLogger(paths.audit);
87
- sayLive(live, `run started: skill=${skillName} agent=${agentName}`);
88
- sayLive(live, `evidence directory: ${paths.runDir}`);
155
+ sayLive(live, `运行开始:skill=${skillName} agent=${agentName}`);
156
+ sayLive(live, `证据目录:${paths.runDir}`);
89
157
  await audit.event({ stage: "intake", event: "run_started", status: "started", detail: { task, skill: skillName, agent: agentName } });
90
158
  const isGitRepo = Boolean(await git(cwd, "rev-parse --show-toplevel"));
91
159
  const intake = {
@@ -103,81 +171,190 @@ async function commandRun(cwd, args) {
103
171
  stage: "intake",
104
172
  event: "git_repo_check",
105
173
  status: "failed",
106
- detail: "Current directory is not a Git repository. Run inside a business Git repository before executing harness run."
174
+ detail: "当前目录不是 Git 仓库。请在业务 Git 仓库内执行 harness run"
107
175
  });
108
176
  await audit.event({ stage: "run", event: "run_finished", status: "failed" });
109
- throw new Error(`harness run must execute inside a Git repository. Evidence: ${paths.runDir}`);
177
+ throw new Error(`harness run 必须在 Git 仓库内执行。证据目录:${paths.runDir}`);
110
178
  }
111
- sayLive(live, "building context pack with MCP first, local fallback second");
112
- await audit.event({ stage: "context_pack", event: "build_started", status: "started" });
113
- const contextPack = await buildContextPack({
114
- cwd,
115
- task,
116
- config,
117
- skill,
118
- contextPackPath: paths.contextPack,
119
- mcpLogPath: paths.mcpQueries,
120
- audit
121
- });
122
- await audit.event({
123
- stage: "context_pack",
124
- event: "build_finished",
125
- status: contextPack.degraded ? "degraded" : "succeeded"
126
- });
127
- sayLive(live, `context pack ${contextPack.degraded ? "degraded" : "ready"}: ${paths.contextPack}`);
128
- const plan = `# Execution Plan
129
-
130
- The selected agent must:
131
- 1. Read the context pack.
132
- 2. Produce impact analysis.
133
- 3. Make the smallest safe change.
134
- 4. Leave enough evidence for review.
135
- 5. Allow the harness to run verification and summarize results.
136
- `;
137
- await writeFile(paths.plan, plan, "utf8");
138
- await audit.event({ stage: "plan", event: "plan_written", status: "succeeded", detail: { path: paths.plan } });
139
- sayLive(live, `plan written: ${paths.plan}`);
140
179
  const adapter = createAgentAdapter(agentName, config);
141
- const agentCommand = agentName === "codex" ? config.agents.codex.command : config.agents.claude.command;
142
- sayLive(live, `executing ${adapter.name}: ${agentCommand}`);
143
- await audit.event({ stage: "execute", event: "agent_started", status: "started", detail: { agent: adapter.name, command: agentCommand } });
144
- let agentResult;
145
- try {
146
- agentResult = await adapter.run({
180
+ const agentCommand = config.agents[agentName].command;
181
+ const maxIterations = Math.max(1, config.loop.maxIterations);
182
+ const evaluations = [];
183
+ let previousFeedback;
184
+ let agentResult = {
185
+ status: "failed",
186
+ exitCode: null,
187
+ stdout: "",
188
+ stderr: "",
189
+ changedFiles: [],
190
+ diff: "",
191
+ error: "Agent 未运行"
192
+ };
193
+ let verifyResult = { status: "skipped", commands: [] };
194
+ let contractCheck = { status: "failed", requiredSections: [], missingSections: [] };
195
+ let verifyDiagnostics;
196
+ let contextScore;
197
+ let approval;
198
+ for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
199
+ const iterationPaths = makeIterationPaths(paths, iteration);
200
+ await ensureDir(iterationPaths.iterationDir);
201
+ await writeFile(iterationPaths.feedback, "", "utf8");
202
+ const checkpoint = await createCheckpoint({
203
+ cwd,
204
+ iteration,
205
+ checkpointPath: iterationPaths.checkpoint,
206
+ preDiffPath: iterationPaths.preIterationDiff,
207
+ runDir: paths.runDir
208
+ });
209
+ await feedbackEvent(iterationPaths.feedback, "checkpoint_created", { iteration, restoreSupported: checkpoint.restoreSupported });
210
+ sayLive(live, `正在构建 Context Pack(自纠偏循环 ${iteration}/${maxIterations})`);
211
+ await audit.event({ stage: "context_pack", event: "build_started", status: "started", detail: { iteration } });
212
+ await buildContextPack({
147
213
  cwd,
148
214
  task,
215
+ config,
149
216
  skill,
150
- contextPackPath: paths.contextPack,
151
- promptPath: paths.agentPrompt,
152
- executionLogPath: paths.executionLog,
153
- live
217
+ contextPackPath: iterationPaths.contextPack,
218
+ mcpLogPath: paths.mcpQueries,
219
+ audit
220
+ });
221
+ await feedbackEvent(iterationPaths.feedback, "context_pack_created", { iteration, path: iterationPaths.contextPack });
222
+ await audit.event({
223
+ stage: "context_pack",
224
+ event: "build_finished",
225
+ status: "succeeded",
226
+ detail: { iteration }
154
227
  });
228
+ sayLive(live, `Context Pack 已生成:${iterationPaths.contextPack}`);
229
+ const plan = `# 执行阶段协议
230
+
231
+ ## 当前轮次
232
+ ${iteration}/${maxIterations}
233
+
234
+ Agent 必须:
235
+ 1. 阅读 context_pack.md。
236
+ 2. 按固定格式输出影响分析。
237
+ 3. 制定最小修改计划。
238
+ 4. 执行最小安全变更。
239
+ 5. 说明验证计划和残余风险。
240
+ 6. 让 Harness 运行验证并生成总结。
241
+ 7. 如果上一轮失败,只围绕失败原因做最小 rework。
242
+ `;
243
+ await writeFile(iterationPaths.plan, plan, "utf8");
244
+ await audit.event({ stage: "plan", event: "plan_written", status: "succeeded", detail: { path: iterationPaths.plan, iteration } });
245
+ sayLive(live, `阶段协议已写入:${iterationPaths.plan}`);
246
+ sayLive(live, `正在执行 ${adapter.name}:${agentCommand}`);
247
+ await feedbackEvent(iterationPaths.feedback, "agent_invoked", { iteration, agent: adapter.name });
155
248
  await audit.event({
156
249
  stage: "execute",
157
- event: "agent_finished",
158
- status: agentResult.status === "success" ? "succeeded" : "failed",
159
- detail: { exitCode: agentResult.exitCode, changedFiles: agentResult.changedFiles, error: agentResult.error }
250
+ event: "agent_started",
251
+ status: "started",
252
+ detail: { agent: adapter.name, command: agentCommand, iteration }
160
253
  });
161
- }
162
- catch (error) {
163
- agentResult = {
164
- status: "failed",
165
- exitCode: null,
166
- stdout: "",
167
- stderr: "",
168
- changedFiles: [],
169
- diff: "",
170
- error: error instanceof Error ? error.message : String(error)
254
+ try {
255
+ agentResult = await adapter.run({
256
+ cwd,
257
+ task,
258
+ skill,
259
+ contextPackPath: iterationPaths.contextPack,
260
+ promptPath: iterationPaths.agentPrompt,
261
+ executionLogPath: iterationPaths.executionLog,
262
+ live,
263
+ iteration,
264
+ previousFeedback
265
+ });
266
+ await audit.event({
267
+ stage: "execute",
268
+ event: "agent_finished",
269
+ status: agentResult.status === "success" ? "succeeded" : "failed",
270
+ detail: { iteration, exitCode: agentResult.exitCode, changedFiles: agentResult.changedFiles, error: agentResult.error }
271
+ });
272
+ }
273
+ catch (error) {
274
+ agentResult = {
275
+ status: "failed",
276
+ exitCode: null,
277
+ stdout: "",
278
+ stderr: "",
279
+ changedFiles: [],
280
+ diff: "",
281
+ error: error instanceof Error ? error.message : String(error)
282
+ };
283
+ await audit.event({ stage: "execute", event: "agent_error", status: "failed", detail: { iteration, error: agentResult.error } });
284
+ }
285
+ await writeFile(iterationPaths.gitDiff, agentResult.diff, "utf8");
286
+ contractCheck = checkAgentContract(`${agentResult.stdout}\n${agentResult.stderr}`);
287
+ await writeJson(iterationPaths.agentContract, contractCheck);
288
+ await feedbackEvent(iterationPaths.feedback, "agent_contract_checked", { iteration, status: contractCheck.status, missingSections: contractCheck.missingSections });
289
+ await audit.event({
290
+ stage: "execute",
291
+ event: "agent_contract_check",
292
+ status: contractCheck.status === "passed" ? "succeeded" : "failed",
293
+ detail: { iteration, contractCheck }
294
+ });
295
+ sayLive(live, `Agent 执行完成:status=${agentResult.status} changedFiles=${agentResult.changedFiles.length} contract=${contractCheck.status}`);
296
+ sayLive(live, "正在运行验证命令");
297
+ await audit.event({ stage: "verify", event: "verify_started", status: "started", detail: { iteration } });
298
+ verifyResult = await runVerify(cwd, config, skill, iterationPaths.verify);
299
+ verifyDiagnostics = await writeVerifyDiagnostics(iterationPaths.verifyDiagnostics, verifyResult);
300
+ await feedbackEvent(iterationPaths.feedback, "verify_command_finished", { iteration, status: verifyResult.status });
301
+ await feedbackEvent(iterationPaths.feedback, "verify_diagnostics_created", { iteration, failedCommands: verifyDiagnostics.failedCommands.length });
302
+ await audit.event({
303
+ stage: "verify",
304
+ event: "verify_finished",
305
+ status: verifyResult.status === "passed" ? "succeeded" : "failed",
306
+ detail: { iteration, verifyResult }
307
+ });
308
+ sayLive(live, `验证完成:${verifyResult.status} ${iterationPaths.verify}`);
309
+ const agentOutput = `${agentResult.stdout}\n${agentResult.stderr}`;
310
+ contextScore = await writeContextScore(iterationPaths.contextScore, agentOutput);
311
+ await feedbackEvent(iterationPaths.feedback, "context_score_created", { iteration, status: contextScore.status, missing: contextScore.missing });
312
+ approval = await evaluateApprovalGate({
313
+ cwd,
314
+ config,
315
+ agentResult,
316
+ agentOutput,
317
+ approvalPath: iterationPaths.approvalRequired
318
+ });
319
+ if (approval.required)
320
+ await feedbackEvent(iterationPaths.feedback, "approval_gate_triggered", { iteration, reasons: approval.reasons });
321
+ agentResult.diff = await writePostIterationDiff(cwd, iterationPaths.postIterationDiff);
322
+ await writeFile(iterationPaths.gitDiff, agentResult.diff, "utf8");
323
+ const evaluation = evaluateIteration({
324
+ iteration,
325
+ maxIterations,
326
+ previous: evaluations.at(-1),
327
+ agentResult,
328
+ contractCheck,
329
+ verifyResult,
330
+ verifyDiagnostics,
331
+ contextScore,
332
+ approval
333
+ });
334
+ evaluations.push(evaluation);
335
+ const loopReview = {
336
+ status: evaluation.status === "passed" ? "passed" : "stopped",
337
+ maxIterations,
338
+ iterations: evaluations
171
339
  };
172
- await audit.event({ stage: "execute", event: "agent_error", status: "failed", detail: agentResult.error });
340
+ await writeLoopArtifacts({ reviewPath: paths.loopReview, reportPath: paths.loopReport, review: loopReview });
341
+ await audit.event({
342
+ stage: "evaluate",
343
+ event: "iteration_evaluated",
344
+ status: evaluation.status === "passed" ? "succeeded" : "failed",
345
+ detail: evaluation
346
+ });
347
+ await feedbackEvent(iterationPaths.feedback, "loop_evaluated", { iteration, status: evaluation.status, failureType: evaluation.failureType });
348
+ await syncIterationSnapshot(paths, iterationPaths);
349
+ sayLive(live, `评估完成:${evaluation.status} failure=${evaluation.failureType}`);
350
+ if (evaluation.nextAction === "summarize")
351
+ break;
352
+ if (evaluation.nextAction === "stop")
353
+ break;
354
+ previousFeedback = renderReworkFeedback(evaluation);
355
+ await audit.event({ stage: "rework", event: "rework_scheduled", status: "started", detail: evaluation });
356
+ sayLive(live, `进入下一轮自纠偏:${evaluation.reason}`);
173
357
  }
174
- await writeFile(paths.gitDiff, agentResult.diff, "utf8");
175
- sayLive(live, `agent finished: status=${agentResult.status} changedFiles=${agentResult.changedFiles.length}`);
176
- sayLive(live, "running verification commands");
177
- await audit.event({ stage: "verify", event: "verify_started", status: "started" });
178
- const verifyResult = await runVerify(cwd, config, skill, paths.verify);
179
- await audit.event({ stage: "verify", event: "verify_finished", status: verifyResult.status === "passed" ? "succeeded" : "failed", detail: verifyResult });
180
- sayLive(live, `verification ${verifyResult.status}: ${paths.verify}`);
181
358
  await writeSummary({
182
359
  task,
183
360
  skillName,
@@ -190,28 +367,30 @@ The selected agent must:
190
367
  });
191
368
  await audit.event({ stage: "summarize", event: "summary_written", status: "succeeded" });
192
369
  await audit.event({ stage: "persist", event: "suggestions_written", status: "succeeded" });
193
- sayLive(live, `summary written: ${paths.summary}`);
194
- sayLive(live, `persist suggestions written: ${paths.persistSuggestions}`);
195
- const runPassed = verifyResult.status === "passed" && agentResult.status === "success";
370
+ sayLive(live, `总结已写入:${paths.summary}`);
371
+ sayLive(live, `知识沉淀建议已写入:${paths.persistSuggestions}`);
372
+ sayLive(live, `自纠偏报告已写入:${paths.loopReport}`);
373
+ const runPassed = evaluations.at(-1)?.status === "passed";
196
374
  await audit.event({ stage: "run", event: "run_finished", status: runPassed ? "succeeded" : "failed" });
197
- console.log(`Run complete: ${paths.runId}`);
198
- console.log(`Evidence: ${paths.runDir}`);
375
+ console.log(`运行完成:${paths.runId}`);
376
+ console.log(`证据目录:${paths.runDir}`);
199
377
  if (!runPassed)
200
- throw new Error(`Harness run failed. Evidence: ${paths.runDir}`);
378
+ throw new Error(`Harness 运行失败。证据目录:${paths.runDir}`);
201
379
  }
202
380
  async function commandInstallIntegration(cwd, args) {
203
381
  const rawAgent = String(args.agent ?? "both");
204
- if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "both") {
205
- throw new Error(`Invalid --agent ${rawAgent}. Expected claude, codex, or both.`);
382
+ if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "cursor" && rawAgent !== "gemini" && rawAgent !== "both" && rawAgent !== "all") {
383
+ throw new Error(`无效 --agent ${rawAgent}。期望 claudecodex、cursor、gemini、both all。`);
206
384
  }
207
385
  const targetDir = typeof args.target === "string" ? path.resolve(cwd, args.target) : cwd;
208
386
  const packageRoot = path.resolve(path.dirname(process.argv[1]), "..");
209
- const harnessCommand = typeof args.command === "string" ? args.command : `node ${quoteCommandPath(path.resolve(process.argv[1]))}`;
210
- const result = await installIntegration({ targetDir, harnessCommand, agent: rawAgent, packageRoot });
211
- console.log(`Installed AI Dev Harness conversation integration in ${targetDir}`);
212
- console.log(`Created: ${result.created.length ? result.created.join(", ") : "(none)"}`);
213
- console.log(`Updated: ${result.updated.length ? result.updated.join(", ") : "(none)"}`);
214
- console.log(`Skipped: ${result.skipped.length ? result.skipped.join(", ") : "(none)"}`);
387
+ const harnessCliPath = path.resolve(process.argv[1]);
388
+ const harnessCommand = typeof args.command === "string" ? args.command : `node ${quoteCommandPath(harnessCliPath)}`;
389
+ const result = await installIntegration({ targetDir, harnessCommand, harnessCliPath, agent: rawAgent, packageRoot });
390
+ console.log(`AI Dev Harness 对话集成已安装:${targetDir}`);
391
+ console.log(`创建:${result.created.length ? result.created.join(", ") : ""}`);
392
+ console.log(`更新:${result.updated.length ? result.updated.join(", ") : ""}`);
393
+ console.log(`跳过:${result.skipped.length ? result.skipped.join(", ") : "无"}`);
215
394
  }
216
395
  async function commandVerify(cwd, args) {
217
396
  const runDir = await resolveRunDir(cwd, requireString(args, "run"));
@@ -219,9 +398,47 @@ async function commandVerify(cwd, args) {
219
398
  const config = await loadConfig(cwd);
220
399
  const skill = await loadSkill(cwd, intake.skill);
221
400
  const result = await runVerify(cwd, config, skill, path.join(runDir, "verify.json"));
401
+ await writeVerifyDiagnostics(path.join(runDir, "verify_diagnostics.json"), result);
222
402
  const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
223
403
  await audit.event({ stage: "verify", event: "verify_rerun", status: result.status === "passed" ? "succeeded" : "failed", detail: result });
224
- console.log(`Verify ${result.status}: ${runDir}`);
404
+ console.log(`验证 ${result.status}:${runDir}`);
405
+ }
406
+ async function commandRestore(cwd, args) {
407
+ const runDir = await resolveRunDir(cwd, requireString(args, "run"));
408
+ const iteration = Number(requireString(args, "iteration"));
409
+ if (!Number.isInteger(iteration) || iteration < 1)
410
+ throw new Error("--iteration 必须是正整数");
411
+ const checkpointPath = path.join(runDir, `iteration-${iteration}`, "checkpoint.json");
412
+ if (!(await pathExists(checkpointPath)))
413
+ throw new Error(`未找到 checkpoint:${checkpointPath}`);
414
+ const checkpoint = JSON.parse(await readFile(checkpointPath, "utf8"));
415
+ const result = await restoreCheckpoint(cwd, checkpoint);
416
+ const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
417
+ await audit.event({ stage: "restore", event: "restore_requested", status: result.ok ? "succeeded" : "failed", detail: { iteration, message: result.message } });
418
+ console.log(result.message);
419
+ if (!result.ok)
420
+ throw new Error(`restore 失败:${result.message}`);
421
+ }
422
+ async function commandRuns(cwd, args) {
423
+ const [subcommand] = args._ ?? [];
424
+ if (subcommand !== "metrics")
425
+ throw new Error(`未知 runs 子命令:${subcommand ?? ""}`);
426
+ const config = await loadConfig(cwd);
427
+ const metrics = await collectRunMetrics(cwd, config);
428
+ if (args.json)
429
+ console.log(JSON.stringify(metrics, null, 2));
430
+ else
431
+ console.log(renderMetricsText(metrics));
432
+ }
433
+ async function commandReplay(cwd, args) {
434
+ const runDir = await resolveRunDir(cwd, requireString(args, "run"));
435
+ const overrideAgent = typeof args.agent === "string" ? requireAgent(args.agent) : undefined;
436
+ const dry = Boolean(args.dry);
437
+ const plan = await buildReplayPlan(runDir, overrideAgent, dry);
438
+ console.log(renderReplayPlan(plan));
439
+ if (dry)
440
+ return;
441
+ await commandRun(cwd, { task: plan.task, skill: plan.skill, agent: plan.agent });
225
442
  }
226
443
  async function commandSummarize(cwd, args) {
227
444
  const runDir = await resolveRunDir(cwd, requireString(args, "run"));
@@ -252,7 +469,7 @@ async function commandSummarize(cwd, args) {
252
469
  });
253
470
  const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
254
471
  await audit.event({ stage: "summarize", event: "summary_rerun", status: "succeeded" });
255
- console.log(`Summary refreshed: ${runDir}`);
472
+ console.log(`总结已刷新:${runDir}`);
256
473
  }
257
474
  async function main() {
258
475
  const cwd = process.cwd();
@@ -263,15 +480,25 @@ async function main() {
263
480
  }
264
481
  if (command === "init")
265
482
  return commandInit(cwd);
483
+ if (command === "doctor")
484
+ return commandDoctor(cwd);
485
+ if (command === "smoke")
486
+ return commandSmoke(cwd, args);
266
487
  if (command === "run")
267
488
  return commandRun(cwd, args);
268
489
  if (command === "verify")
269
490
  return commandVerify(cwd, args);
270
491
  if (command === "summarize")
271
492
  return commandSummarize(cwd, args);
493
+ if (command === "restore")
494
+ return commandRestore(cwd, args);
495
+ if (command === "runs")
496
+ return commandRuns(cwd, args);
497
+ if (command === "replay")
498
+ return commandReplay(cwd, args);
272
499
  if (command === "install-integration")
273
500
  return commandInstallIntegration(cwd, args);
274
- throw new Error(`Unknown command: ${command}\n${usage()}`);
501
+ throw new Error(`未知命令:${command}\n${usage()}`);
275
502
  }
276
503
  main().catch((error) => {
277
504
  console.error(error instanceof Error ? error.message : String(error));