ai-dev-harness 0.1.1 → 0.1.3

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/dist/cli.js CHANGED
@@ -1,26 +1,69 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { copyFile, readFile, writeFile } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { AuditLogger, prepareRunFiles, writeJson } from "./audit.js";
5
6
  import { checkAgentContract, createAgentAdapter } from "./agent.js";
7
+ import { evaluateApprovalGate } from "./approval.js";
8
+ import { createCheckpoint, restoreCheckpoint, writePostIterationDiff } from "./checkpoint.js";
6
9
  import { loadConfig, loadSkill } from "./config.js";
7
10
  import { buildContextPack } from "./context.js";
11
+ import { writeContextScore } from "./contextScore.js";
12
+ import { writeVerifyDiagnostics } from "./diagnostics.js";
13
+ import { runDoctor } from "./doctor.js";
14
+ import { feedbackEvent } from "./feedback.js";
8
15
  import { initHarness } from "./init.js";
9
16
  import { installIntegration } from "./integration.js";
10
- import { makeRunPaths } from "./runPaths.js";
17
+ import { evaluateIteration, renderReworkFeedback, writeLoopArtifacts } from "./loop.js";
18
+ import { collectRunMetrics, renderMetricsText } from "./metrics.js";
19
+ import { buildReplayPlan, renderReplayPlan } from "./replay.js";
20
+ import { makeIterationPaths, makeRunPaths } from "./runPaths.js";
11
21
  import { writeSummary } from "./summarize.js";
12
22
  import { runVerify } from "./verify.js";
13
- import { git, pathExists } from "./utils.js";
23
+ import { ensureDir, git, pathExists } from "./utils.js";
24
+ const booleanArgs = new Set(["dry", "json", "live", "no-integration"]);
25
+ const require = createRequire(import.meta.url);
26
+ const packageVersion = String(require("../package.json").version);
27
+ function normalizeRel(file) {
28
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
29
+ }
30
+ function statusFiles(status) {
31
+ return new Set(status
32
+ .split(/\r?\n/)
33
+ .map((line) => line.trim())
34
+ .filter(Boolean)
35
+ .map((line) => {
36
+ const rename = line.match(/^R.?\s+(.+)\s+->\s+(.+)$/);
37
+ if (rename)
38
+ return normalizeRel(rename[2]);
39
+ return normalizeRel(line.slice(3).trim());
40
+ })
41
+ .filter(Boolean));
42
+ }
43
+ function underRoot(file, root) {
44
+ const normalized = normalizeRel(file);
45
+ const normalizedRoot = normalizeRel(root).replace(/\/?$/, "/");
46
+ return normalized === normalizedRoot.slice(0, -1) || normalized.startsWith(normalizedRoot);
47
+ }
48
+ function excludeBaselineChangedFiles(changedFiles, baselineStatus, excludedRoots = []) {
49
+ const baselineFiles = statusFiles(baselineStatus);
50
+ return changedFiles.filter((file) => {
51
+ const normalized = normalizeRel(file);
52
+ return !baselineFiles.has(normalized) && !excludedRoots.some((root) => underRoot(normalized, root));
53
+ });
54
+ }
14
55
  function parseArgs(argv) {
15
56
  const [command = "help", ...rest] = argv;
16
- const args = {};
57
+ const args = { _: [] };
17
58
  for (let i = 0; i < rest.length; i += 1) {
18
59
  const item = rest[i];
19
- if (!item.startsWith("--"))
60
+ if (!item.startsWith("--")) {
61
+ args._.push(item);
20
62
  continue;
63
+ }
21
64
  const key = item.slice(2);
22
65
  const next = rest[i + 1];
23
- if (!next || next.startsWith("--"))
66
+ if (booleanArgs.has(key) || !next || next.startsWith("--"))
24
67
  args[key] = true;
25
68
  else {
26
69
  args[key] = next;
@@ -35,13 +78,36 @@ function usage() {
35
78
  用法:
36
79
  harness init
37
80
  harness init --no-integration
38
- harness run --task <text> --skill <name> --agent <codex|claude>
39
- harness run --task <text> --skill <name> --agent <codex|claude> --live
81
+ harness --version
82
+ harness doctor
83
+ harness smoke --agent <codex|claude|cursor|gemini> [--live]
84
+ harness run --task <text> --skill <name> --agent <codex|claude|cursor|gemini>
85
+ harness run --task <text> --skill <name> --agent <codex|claude|cursor|gemini> --live
40
86
  harness verify --run <run_id_or_path>
41
87
  harness summarize --run <run_id_or_path>
42
- harness install-integration --agent <claude|codex|both> [--target <repo>] [--command <harness command>]
88
+ harness restore --run <run_id_or_path> --iteration <n>
89
+ harness runs metrics [--json]
90
+ harness replay --run <run_id_or_path> [--dry] [--agent <codex|claude|cursor|gemini>]
91
+ harness install-integration --agent <claude|codex|cursor|gemini|both|all> [--target <repo>] [--command <harness command>]
43
92
  `;
44
93
  }
94
+ async function commandDoctor(cwd) {
95
+ const config = await loadConfig(cwd);
96
+ const result = await runDoctor(cwd, config);
97
+ console.log(result.text);
98
+ if (result.failed)
99
+ throw new Error("doctor 检查失败,请先修复 FAIL 项。");
100
+ }
101
+ async function commandSmoke(cwd, args) {
102
+ const agentName = requireAgent(requireString(args, "agent"));
103
+ const live = Boolean(args.live);
104
+ await commandRun(cwd, {
105
+ task: "Smoke test only. Analyze repository context and do not modify repository files. You must output these exact top-level headings in this order: ## Impact Analysis, ## Plan, ## Execution Notes, ## Verification Notes, ## Final Report.",
106
+ skill: "update_docs",
107
+ agent: agentName,
108
+ live
109
+ });
110
+ }
45
111
  function requireString(args, key) {
46
112
  const value = args[key];
47
113
  if (typeof value !== "string" || !value.trim())
@@ -49,8 +115,9 @@ function requireString(args, key) {
49
115
  return value;
50
116
  }
51
117
  function requireAgent(value) {
52
- if (value !== "codex" && value !== "claude")
53
- throw new Error(`无效 agent:${value}。期望 codex 或 claude。`);
118
+ if (value !== "codex" && value !== "claude" && value !== "cursor" && value !== "gemini") {
119
+ throw new Error(`无效 agent:${value}。期望 codex、claude、cursorgemini。`);
120
+ }
54
121
  return value;
55
122
  }
56
123
  function quoteCommandPath(filePath) {
@@ -72,13 +139,13 @@ async function commandInit(cwd) {
72
139
  console.log(`AI Dev Harness 初始化完成:${cwd}`);
73
140
  console.log(`创建 ${result.created.length} 个文件,跳过 ${result.skipped.length} 个已存在文件。`);
74
141
  if (process.argv.includes("--no-integration")) {
75
- console.log("已跳过 Claude/Codex 对话集成。");
142
+ console.log("已跳过 Agent 对话集成。");
76
143
  return;
77
144
  }
78
145
  const harnessCliPath = path.resolve(process.argv[1]);
79
146
  const harnessCommand = `node ${quoteCommandPath(harnessCliPath)}`;
80
- const integration = await installIntegration({ targetDir: cwd, harnessCommand, harnessCliPath, agent: "both", packageRoot });
81
- console.log("Claude/Codex 对话集成已安装。");
147
+ const integration = await installIntegration({ targetDir: cwd, harnessCommand, harnessCliPath, agent: "all", packageRoot });
148
+ console.log("Claude/Codex/Cursor/Gemini 对话集成已安装。");
82
149
  console.log(`集成创建:${integration.created.length ? integration.created.join(", ") : "无"}`);
83
150
  console.log(`集成更新:${integration.updated.length ? integration.updated.join(", ") : "无"}`);
84
151
  console.log(`集成跳过:${integration.skipped.length ? integration.skipped.join(", ") : "无"}`);
@@ -87,6 +154,26 @@ function sayLive(live, message) {
87
154
  if (live)
88
155
  console.log(`[harness] ${message}`);
89
156
  }
157
+ async function copyIfExists(from, to) {
158
+ if (await pathExists(from))
159
+ await copyFile(from, to);
160
+ }
161
+ async function syncIterationSnapshot(paths, iterationPaths) {
162
+ await copyIfExists(iterationPaths.contextPack, paths.contextPack);
163
+ await copyIfExists(iterationPaths.agentPrompt, paths.agentPrompt);
164
+ await copyIfExists(iterationPaths.plan, paths.plan);
165
+ await copyIfExists(iterationPaths.executionLog, paths.executionLog);
166
+ await copyIfExists(iterationPaths.agentContract, paths.agentContract);
167
+ await copyIfExists(iterationPaths.gitDiff, paths.gitDiff);
168
+ await copyIfExists(iterationPaths.verify, paths.verify);
169
+ await copyIfExists(iterationPaths.verifyDiagnostics, paths.verifyDiagnostics);
170
+ await copyIfExists(iterationPaths.contextScore, paths.contextScore);
171
+ await copyIfExists(iterationPaths.approvalRequired, paths.approvalRequired);
172
+ await copyIfExists(iterationPaths.feedback, paths.feedback);
173
+ await copyIfExists(iterationPaths.checkpoint, paths.checkpoint);
174
+ await copyIfExists(iterationPaths.preIterationDiff, paths.preIterationDiff);
175
+ await copyIfExists(iterationPaths.postIterationDiff, paths.postIterationDiff);
176
+ }
90
177
  async function commandRun(cwd, args) {
91
178
  const task = requireString(args, "task");
92
179
  const skillName = requireString(args, "skill");
@@ -121,24 +208,60 @@ async function commandRun(cwd, args) {
121
208
  await audit.event({ stage: "run", event: "run_finished", status: "failed" });
122
209
  throw new Error(`harness run 必须在 Git 仓库内执行。证据目录:${paths.runDir}`);
123
210
  }
124
- sayLive(live, "正在构建 Context Pack:包含 Agent MCP 指引和本地兜底上下文");
125
- await audit.event({ stage: "context_pack", event: "build_started", status: "started" });
126
- const contextPack = await buildContextPack({
127
- cwd,
128
- task,
129
- config,
130
- skill,
131
- contextPackPath: paths.contextPack,
132
- mcpLogPath: paths.mcpQueries,
133
- audit
134
- });
135
- await audit.event({
136
- stage: "context_pack",
137
- event: "build_finished",
138
- status: "succeeded"
139
- });
140
- sayLive(live, `Context Pack 已生成:${paths.contextPack}`);
141
- const plan = `# 执行阶段协议
211
+ const adapter = createAgentAdapter(agentName, config);
212
+ const agentCommand = config.agents[agentName].command;
213
+ const maxIterations = Math.max(1, config.loop.maxIterations);
214
+ const evaluations = [];
215
+ let previousFeedback;
216
+ let agentResult = {
217
+ status: "failed",
218
+ exitCode: null,
219
+ stdout: "",
220
+ stderr: "",
221
+ changedFiles: [],
222
+ diff: "",
223
+ error: "Agent 未运行"
224
+ };
225
+ let verifyResult = { status: "skipped", commands: [] };
226
+ let contractCheck = { status: "failed", requiredSections: [], missingSections: [] };
227
+ let verifyDiagnostics;
228
+ let contextScore;
229
+ let approval;
230
+ for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
231
+ const iterationPaths = makeIterationPaths(paths, iteration);
232
+ await ensureDir(iterationPaths.iterationDir);
233
+ await writeFile(iterationPaths.feedback, "", "utf8");
234
+ const checkpoint = await createCheckpoint({
235
+ cwd,
236
+ iteration,
237
+ checkpointPath: iterationPaths.checkpoint,
238
+ preDiffPath: iterationPaths.preIterationDiff,
239
+ runDir: paths.runDir
240
+ });
241
+ await feedbackEvent(iterationPaths.feedback, "checkpoint_created", { iteration, restoreSupported: checkpoint.restoreSupported });
242
+ sayLive(live, `正在构建 Context Pack(自纠偏循环 ${iteration}/${maxIterations})`);
243
+ await audit.event({ stage: "context_pack", event: "build_started", status: "started", detail: { iteration } });
244
+ await buildContextPack({
245
+ cwd,
246
+ task,
247
+ config,
248
+ skill,
249
+ contextPackPath: iterationPaths.contextPack,
250
+ mcpLogPath: paths.mcpQueries,
251
+ audit
252
+ });
253
+ await feedbackEvent(iterationPaths.feedback, "context_pack_created", { iteration, path: iterationPaths.contextPack });
254
+ await audit.event({
255
+ stage: "context_pack",
256
+ event: "build_finished",
257
+ status: "succeeded",
258
+ detail: { iteration }
259
+ });
260
+ sayLive(live, `Context Pack 已生成:${iterationPaths.contextPack}`);
261
+ const plan = `# 执行阶段协议
262
+
263
+ ## 当前轮次
264
+ ${iteration}/${maxIterations}
142
265
 
143
266
  Agent 必须:
144
267
  1. 阅读 context_pack.md。
@@ -147,59 +270,124 @@ Agent 必须:
147
270
  4. 执行最小安全变更。
148
271
  5. 说明验证计划和残余风险。
149
272
  6. 让 Harness 运行验证并生成总结。
273
+ 7. 如果上一轮失败,只围绕失败原因做最小 rework。
150
274
  `;
151
- await writeFile(paths.plan, plan, "utf8");
152
- await audit.event({ stage: "plan", event: "plan_written", status: "succeeded", detail: { path: paths.plan } });
153
- sayLive(live, `阶段协议已写入:${paths.plan}`);
154
- const adapter = createAgentAdapter(agentName, config);
155
- const agentCommand = agentName === "codex" ? config.agents.codex.command : config.agents.claude.command;
156
- sayLive(live, `正在执行 ${adapter.name}:${agentCommand}`);
157
- await audit.event({ stage: "execute", event: "agent_started", status: "started", detail: { agent: adapter.name, command: agentCommand } });
158
- let agentResult;
159
- try {
160
- agentResult = await adapter.run({
161
- cwd,
162
- task,
163
- skill,
164
- contextPackPath: paths.contextPack,
165
- promptPath: paths.agentPrompt,
166
- executionLogPath: paths.executionLog,
167
- live
275
+ await writeFile(iterationPaths.plan, plan, "utf8");
276
+ await audit.event({ stage: "plan", event: "plan_written", status: "succeeded", detail: { path: iterationPaths.plan, iteration } });
277
+ sayLive(live, `阶段协议已写入:${iterationPaths.plan}`);
278
+ sayLive(live, `正在执行 ${adapter.name}:${agentCommand}`);
279
+ await feedbackEvent(iterationPaths.feedback, "agent_invoked", { iteration, agent: adapter.name });
280
+ await audit.event({
281
+ stage: "execute",
282
+ event: "agent_started",
283
+ status: "started",
284
+ detail: { agent: adapter.name, command: agentCommand, iteration }
168
285
  });
286
+ try {
287
+ agentResult = await adapter.run({
288
+ cwd,
289
+ task,
290
+ skill,
291
+ contextPackPath: iterationPaths.contextPack,
292
+ promptPath: iterationPaths.agentPrompt,
293
+ executionLogPath: iterationPaths.executionLog,
294
+ live,
295
+ iteration,
296
+ previousFeedback
297
+ });
298
+ agentResult.changedFiles = excludeBaselineChangedFiles(agentResult.changedFiles, checkpoint.gitStatusBefore, checkpoint.excludedStatusRoots);
299
+ await audit.event({
300
+ stage: "execute",
301
+ event: "agent_finished",
302
+ status: agentResult.status === "success" ? "succeeded" : "failed",
303
+ detail: { iteration, exitCode: agentResult.exitCode, changedFiles: agentResult.changedFiles, error: agentResult.error }
304
+ });
305
+ }
306
+ catch (error) {
307
+ agentResult = {
308
+ status: "failed",
309
+ exitCode: null,
310
+ stdout: "",
311
+ stderr: "",
312
+ changedFiles: [],
313
+ diff: "",
314
+ error: error instanceof Error ? error.message : String(error)
315
+ };
316
+ await audit.event({ stage: "execute", event: "agent_error", status: "failed", detail: { iteration, error: agentResult.error } });
317
+ }
318
+ await writeFile(iterationPaths.gitDiff, agentResult.diff, "utf8");
319
+ contractCheck = checkAgentContract(`${agentResult.stdout}\n${agentResult.stderr}`);
320
+ await writeJson(iterationPaths.agentContract, contractCheck);
321
+ await feedbackEvent(iterationPaths.feedback, "agent_contract_checked", { iteration, status: contractCheck.status, missingSections: contractCheck.missingSections });
169
322
  await audit.event({
170
323
  stage: "execute",
171
- event: "agent_finished",
172
- status: agentResult.status === "success" ? "succeeded" : "failed",
173
- detail: { exitCode: agentResult.exitCode, changedFiles: agentResult.changedFiles, error: agentResult.error }
324
+ event: "agent_contract_check",
325
+ status: contractCheck.status === "passed" ? "succeeded" : "failed",
326
+ detail: { iteration, contractCheck }
174
327
  });
175
- }
176
- catch (error) {
177
- agentResult = {
178
- status: "failed",
179
- exitCode: null,
180
- stdout: "",
181
- stderr: "",
182
- changedFiles: [],
183
- diff: "",
184
- error: error instanceof Error ? error.message : String(error)
328
+ sayLive(live, `Agent 执行完成:status=${agentResult.status} changedFiles=${agentResult.changedFiles.length} contract=${contractCheck.status}`);
329
+ sayLive(live, "正在运行验证命令");
330
+ await audit.event({ stage: "verify", event: "verify_started", status: "started", detail: { iteration } });
331
+ verifyResult = await runVerify(cwd, config, skill, iterationPaths.verify);
332
+ verifyDiagnostics = await writeVerifyDiagnostics(iterationPaths.verifyDiagnostics, verifyResult);
333
+ await feedbackEvent(iterationPaths.feedback, "verify_command_finished", { iteration, status: verifyResult.status });
334
+ await feedbackEvent(iterationPaths.feedback, "verify_diagnostics_created", { iteration, failedCommands: verifyDiagnostics.failedCommands.length });
335
+ await audit.event({
336
+ stage: "verify",
337
+ event: "verify_finished",
338
+ status: verifyResult.status === "passed" ? "succeeded" : "failed",
339
+ detail: { iteration, verifyResult }
340
+ });
341
+ sayLive(live, `验证完成:${verifyResult.status} ${iterationPaths.verify}`);
342
+ const agentOutput = `${agentResult.stdout}\n${agentResult.stderr}`;
343
+ contextScore = await writeContextScore(iterationPaths.contextScore, agentOutput);
344
+ await feedbackEvent(iterationPaths.feedback, "context_score_created", { iteration, status: contextScore.status, missing: contextScore.missing });
345
+ approval = await evaluateApprovalGate({
346
+ cwd,
347
+ config,
348
+ agentResult,
349
+ agentOutput,
350
+ approvalPath: iterationPaths.approvalRequired
351
+ });
352
+ if (approval.required)
353
+ await feedbackEvent(iterationPaths.feedback, "approval_gate_triggered", { iteration, reasons: approval.reasons });
354
+ agentResult.diff = await writePostIterationDiff(cwd, iterationPaths.postIterationDiff);
355
+ await writeFile(iterationPaths.gitDiff, agentResult.diff, "utf8");
356
+ const evaluation = evaluateIteration({
357
+ iteration,
358
+ maxIterations,
359
+ previous: evaluations.at(-1),
360
+ agentResult,
361
+ contractCheck,
362
+ verifyResult,
363
+ verifyDiagnostics,
364
+ contextScore,
365
+ approval
366
+ });
367
+ evaluations.push(evaluation);
368
+ const loopReview = {
369
+ status: evaluation.status === "passed" ? "passed" : "stopped",
370
+ maxIterations,
371
+ iterations: evaluations
185
372
  };
186
- await audit.event({ stage: "execute", event: "agent_error", status: "failed", detail: agentResult.error });
373
+ await writeLoopArtifacts({ reviewPath: paths.loopReview, reportPath: paths.loopReport, review: loopReview });
374
+ await audit.event({
375
+ stage: "evaluate",
376
+ event: "iteration_evaluated",
377
+ status: evaluation.status === "passed" ? "succeeded" : "failed",
378
+ detail: evaluation
379
+ });
380
+ await feedbackEvent(iterationPaths.feedback, "loop_evaluated", { iteration, status: evaluation.status, failureType: evaluation.failureType });
381
+ await syncIterationSnapshot(paths, iterationPaths);
382
+ sayLive(live, `评估完成:${evaluation.status} failure=${evaluation.failureType}`);
383
+ if (evaluation.nextAction === "summarize")
384
+ break;
385
+ if (evaluation.nextAction === "stop")
386
+ break;
387
+ previousFeedback = renderReworkFeedback(evaluation);
388
+ await audit.event({ stage: "rework", event: "rework_scheduled", status: "started", detail: evaluation });
389
+ sayLive(live, `进入下一轮自纠偏:${evaluation.reason}`);
187
390
  }
188
- await writeFile(paths.gitDiff, agentResult.diff, "utf8");
189
- const contractCheck = checkAgentContract(`${agentResult.stdout}\n${agentResult.stderr}`);
190
- await writeJson(paths.agentContract, contractCheck);
191
- await audit.event({
192
- stage: "execute",
193
- event: "agent_contract_check",
194
- status: contractCheck.status === "passed" ? "succeeded" : "failed",
195
- detail: contractCheck
196
- });
197
- sayLive(live, `Agent 执行完成:status=${agentResult.status} changedFiles=${agentResult.changedFiles.length} contract=${contractCheck.status}`);
198
- sayLive(live, "正在运行验证命令");
199
- await audit.event({ stage: "verify", event: "verify_started", status: "started" });
200
- const verifyResult = await runVerify(cwd, config, skill, paths.verify);
201
- await audit.event({ stage: "verify", event: "verify_finished", status: verifyResult.status === "passed" ? "succeeded" : "failed", detail: verifyResult });
202
- sayLive(live, `验证完成:${verifyResult.status} ${paths.verify}`);
203
391
  await writeSummary({
204
392
  task,
205
393
  skillName,
@@ -214,7 +402,8 @@ Agent 必须:
214
402
  await audit.event({ stage: "persist", event: "suggestions_written", status: "succeeded" });
215
403
  sayLive(live, `总结已写入:${paths.summary}`);
216
404
  sayLive(live, `知识沉淀建议已写入:${paths.persistSuggestions}`);
217
- const runPassed = verifyResult.status === "passed" && agentResult.status === "success";
405
+ sayLive(live, `自纠偏报告已写入:${paths.loopReport}`);
406
+ const runPassed = evaluations.at(-1)?.status === "passed";
218
407
  await audit.event({ stage: "run", event: "run_finished", status: runPassed ? "succeeded" : "failed" });
219
408
  console.log(`运行完成:${paths.runId}`);
220
409
  console.log(`证据目录:${paths.runDir}`);
@@ -223,8 +412,8 @@ Agent 必须:
223
412
  }
224
413
  async function commandInstallIntegration(cwd, args) {
225
414
  const rawAgent = String(args.agent ?? "both");
226
- if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "both") {
227
- throw new Error(`无效 --agent ${rawAgent}。期望 claude、codex 或 both。`);
415
+ if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "cursor" && rawAgent !== "gemini" && rawAgent !== "both" && rawAgent !== "all") {
416
+ throw new Error(`无效 --agent ${rawAgent}。期望 claude、codex、cursor、gemini、bothall。`);
228
417
  }
229
418
  const targetDir = typeof args.target === "string" ? path.resolve(cwd, args.target) : cwd;
230
419
  const packageRoot = path.resolve(path.dirname(process.argv[1]), "..");
@@ -242,10 +431,48 @@ async function commandVerify(cwd, args) {
242
431
  const config = await loadConfig(cwd);
243
432
  const skill = await loadSkill(cwd, intake.skill);
244
433
  const result = await runVerify(cwd, config, skill, path.join(runDir, "verify.json"));
434
+ await writeVerifyDiagnostics(path.join(runDir, "verify_diagnostics.json"), result);
245
435
  const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
246
436
  await audit.event({ stage: "verify", event: "verify_rerun", status: result.status === "passed" ? "succeeded" : "failed", detail: result });
247
437
  console.log(`验证 ${result.status}:${runDir}`);
248
438
  }
439
+ async function commandRestore(cwd, args) {
440
+ const runDir = await resolveRunDir(cwd, requireString(args, "run"));
441
+ const iteration = Number(requireString(args, "iteration"));
442
+ if (!Number.isInteger(iteration) || iteration < 1)
443
+ throw new Error("--iteration 必须是正整数");
444
+ const checkpointPath = path.join(runDir, `iteration-${iteration}`, "checkpoint.json");
445
+ if (!(await pathExists(checkpointPath)))
446
+ throw new Error(`未找到 checkpoint:${checkpointPath}`);
447
+ const checkpoint = JSON.parse(await readFile(checkpointPath, "utf8"));
448
+ const result = await restoreCheckpoint(cwd, checkpoint);
449
+ const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
450
+ await audit.event({ stage: "restore", event: "restore_requested", status: result.ok ? "succeeded" : "failed", detail: { iteration, message: result.message } });
451
+ console.log(result.message);
452
+ if (!result.ok)
453
+ throw new Error(`restore 失败:${result.message}`);
454
+ }
455
+ async function commandRuns(cwd, args) {
456
+ const [subcommand] = args._ ?? [];
457
+ if (subcommand !== "metrics")
458
+ throw new Error(`未知 runs 子命令:${subcommand ?? ""}`);
459
+ const config = await loadConfig(cwd);
460
+ const metrics = await collectRunMetrics(cwd, config);
461
+ if (args.json)
462
+ console.log(JSON.stringify(metrics, null, 2));
463
+ else
464
+ console.log(renderMetricsText(metrics));
465
+ }
466
+ async function commandReplay(cwd, args) {
467
+ const runDir = await resolveRunDir(cwd, requireString(args, "run"));
468
+ const overrideAgent = typeof args.agent === "string" ? requireAgent(args.agent) : undefined;
469
+ const dry = Boolean(args.dry);
470
+ const plan = await buildReplayPlan(runDir, overrideAgent, dry);
471
+ console.log(renderReplayPlan(plan));
472
+ if (dry)
473
+ return;
474
+ await commandRun(cwd, { task: plan.task, skill: plan.skill, agent: plan.agent });
475
+ }
249
476
  async function commandSummarize(cwd, args) {
250
477
  const runDir = await resolveRunDir(cwd, requireString(args, "run"));
251
478
  const intake = JSON.parse(await readFile(path.join(runDir, "intake.json"), "utf8"));
@@ -280,18 +507,32 @@ async function commandSummarize(cwd, args) {
280
507
  async function main() {
281
508
  const cwd = process.cwd();
282
509
  const { command, args } = parseArgs(process.argv.slice(2));
510
+ if (command === "--version" || command === "-v" || command === "version") {
511
+ console.log(packageVersion);
512
+ return;
513
+ }
283
514
  if (command === "help" || command === "--help" || command === "-h") {
284
515
  console.log(usage());
285
516
  return;
286
517
  }
287
518
  if (command === "init")
288
519
  return commandInit(cwd);
520
+ if (command === "doctor")
521
+ return commandDoctor(cwd);
522
+ if (command === "smoke")
523
+ return commandSmoke(cwd, args);
289
524
  if (command === "run")
290
525
  return commandRun(cwd, args);
291
526
  if (command === "verify")
292
527
  return commandVerify(cwd, args);
293
528
  if (command === "summarize")
294
529
  return commandSummarize(cwd, args);
530
+ if (command === "restore")
531
+ return commandRestore(cwd, args);
532
+ if (command === "runs")
533
+ return commandRuns(cwd, args);
534
+ if (command === "replay")
535
+ return commandReplay(cwd, args);
295
536
  if (command === "install-integration")
296
537
  return commandInstallIntegration(cwd, args);
297
538
  throw new Error(`未知命令:${command}\n${usage()}`);
package/dist/config.js CHANGED
@@ -4,7 +4,9 @@ import { pathExists, readTextIfExists } from "./utils.js";
4
4
  export const defaultConfig = {
5
5
  agents: {
6
6
  codex: { command: "codex exec --full-auto -" },
7
- claude: { command: "claude -p -" }
7
+ claude: { command: "claude -p -" },
8
+ cursor: { command: "cursor-agent" },
9
+ gemini: { command: "gemini" }
8
10
  },
9
11
  mcp: {
10
12
  codegraph: { type: "http", url: "http://localhost:7331/mcp", headers: {} },
@@ -18,6 +20,21 @@ export const defaultConfig = {
18
20
  context: {
19
21
  fallbackInclude: ["AGENTS.md", "TOOLS.md", "SKILLS.md", "README.md", "package.json", "src"],
20
22
  exclude: ["node_modules", ".git", "dist", "build", ".next", "coverage", "runs"]
23
+ },
24
+ loop: {
25
+ maxIterations: 1,
26
+ stopOn: ["same_failure_repeated", "max_iterations_reached"]
27
+ },
28
+ policy: {
29
+ protectedPaths: [".github/", "scripts/release/", "security/"],
30
+ requireApproval: [
31
+ "protected_path_change",
32
+ "dependency_change",
33
+ "delete_files",
34
+ "public_api_change_declared",
35
+ "security_change_declared"
36
+ ],
37
+ approvalMode: "file"
21
38
  }
22
39
  };
23
40
  export function renderYaml(value, indent = 0) {
@@ -163,6 +180,8 @@ export async function loadConfig(cwd) {
163
180
  const verifyRoot = topSection(text, "verify");
164
181
  const verifySection = verifyRoot.match(/defaultCommands:\n([\s\S]*?)(?=^\S|^ [a-zA-Z][\w]*:|$)/m)?.[1] ?? "";
165
182
  const contextSection = topSection(text, "context");
183
+ const loopSection = topSection(text, "loop");
184
+ const policySection = topSection(text, "policy");
166
185
  const codegraphSection = indentedSection(text, "codegraph", 2);
167
186
  const knowledgeSection = indentedSection(text, "knowledgeGraph", 2);
168
187
  return {
@@ -172,6 +191,12 @@ export async function loadConfig(cwd) {
172
191
  },
173
192
  claude: {
174
193
  command: String(scalar(text, /agents:\n[\s\S]*?claude:\n\s+command:\s*([^\n]+)/m, defaultConfig.agents.claude.command))
194
+ },
195
+ cursor: {
196
+ command: String(scalar(text, /agents:\n[\s\S]*?cursor:\n\s+command:\s*([^\n]+)/m, defaultConfig.agents.cursor.command))
197
+ },
198
+ gemini: {
199
+ command: String(scalar(text, /agents:\n[\s\S]*?gemini:\n\s+command:\s*([^\n]+)/m, defaultConfig.agents.gemini.command))
175
200
  }
176
201
  },
177
202
  mcp: {
@@ -200,6 +225,19 @@ export async function loadConfig(cwd) {
200
225
  exclude: blockList(contextSection, "exclude").length
201
226
  ? blockList(contextSection, "exclude")
202
227
  : defaultConfig.context.exclude
228
+ },
229
+ loop: {
230
+ maxIterations: Number(scalar(loopSection, /maxIterations:\s*([^\n]+)/m, defaultConfig.loop.maxIterations)),
231
+ stopOn: blockList(loopSection, "stopOn").length ? blockList(loopSection, "stopOn") : defaultConfig.loop.stopOn
232
+ },
233
+ policy: {
234
+ protectedPaths: blockList(policySection, "protectedPaths").length
235
+ ? blockList(policySection, "protectedPaths")
236
+ : defaultConfig.policy.protectedPaths,
237
+ requireApproval: blockList(policySection, "requireApproval").length
238
+ ? blockList(policySection, "requireApproval")
239
+ : defaultConfig.policy.requireApproval,
240
+ approvalMode: "file"
203
241
  }
204
242
  };
205
243
  }
package/dist/context.js CHANGED
@@ -57,7 +57,9 @@ export async function buildContextPack(options) {
57
57
  const markdown = `# Context Pack
58
58
 
59
59
  ## How To Use This Context
60
- - CodeGraph and Knowledge Graph are agent-side MCP sources. Use Claude/Codex MCP tools directly before and during execution when available.
60
+ - CodeGraph and Knowledge Graph are agent-side MCP sources. Use the current execution agent's MCP tools directly before and during execution when available.
61
+ - For colbymchenry/codegraph, prefer the default codegraph_explore tool first. Ask it with the suspected flow, file, symbol, or behavior; use its grouped source, line-numbered source, call paths, dynamic dispatch hops, and blast-radius summary to build context.
62
+ - For browser_knowledge_service, use progressive disclosure: list_path/search_docs/search_sections/search_chunks or hybrid_search/answer_context first, then read_doc_outline, then read_section/read_doc_range/read_chunk/get_related_chunks for exact evidence. Do not call propose_doc in Harness v1; write suggestions to persist_suggestions.md.
61
63
  - Harness records MCP configuration and audit evidence, but it does not implement MCP transport or replace the agent MCP client.
62
64
  - If the current agent cannot access an MCP source, explicitly mention the missing source and rely on fallback context carefully.
63
65
  - Do not assume fallback file snippets are complete repository knowledge.
@@ -69,17 +71,24 @@ Before editing, assemble context in this exact shape:
69
71
 
70
72
  ### MCP Preflight Findings
71
73
  - CodeGraph tools used:
74
+ - CodeGraph primary query:
72
75
  - CodeGraph findings:
73
- - Related files:
74
- - Related symbols:
75
- - Callers / dependencies:
76
- - Impact scope:
76
+ - Source grouped by file:
77
+ - Current line-numbered source:
78
+ - Call paths:
79
+ - Dynamic dispatch hops:
80
+ - Blast-radius summary:
77
81
  - Knowledge Graph tools used:
78
82
  - Knowledge Graph findings:
83
+ - Search tools used:
84
+ - Candidate concept_ids:
85
+ - Read tools used:
86
+ - Evidence sections/chunks:
79
87
  - ADRs / decisions:
80
88
  - Module notes:
81
89
  - API notes:
82
90
  - Runbooks / known issues:
91
+ - Audit/backlink findings:
83
92
  - Missing context:
84
93
 
85
94
  ### Impact Analysis