ai-dev-harness 0.1.1 → 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.
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
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;
@@ -35,13 +47,35 @@ function usage() {
35
47
  用法:
36
48
  harness init
37
49
  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
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
40
54
  harness verify --run <run_id_or_path>
41
55
  harness summarize --run <run_id_or_path>
42
- 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>]
43
60
  `;
44
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
+ }
45
79
  function requireString(args, key) {
46
80
  const value = args[key];
47
81
  if (typeof value !== "string" || !value.trim())
@@ -49,8 +83,9 @@ function requireString(args, key) {
49
83
  return value;
50
84
  }
51
85
  function requireAgent(value) {
52
- if (value !== "codex" && value !== "claude")
53
- throw new Error(`无效 agent:${value}。期望 codex 或 claude。`);
86
+ if (value !== "codex" && value !== "claude" && value !== "cursor" && value !== "gemini") {
87
+ throw new Error(`无效 agent:${value}。期望 codex、claude、cursorgemini。`);
88
+ }
54
89
  return value;
55
90
  }
56
91
  function quoteCommandPath(filePath) {
@@ -72,13 +107,13 @@ async function commandInit(cwd) {
72
107
  console.log(`AI Dev Harness 初始化完成:${cwd}`);
73
108
  console.log(`创建 ${result.created.length} 个文件,跳过 ${result.skipped.length} 个已存在文件。`);
74
109
  if (process.argv.includes("--no-integration")) {
75
- console.log("已跳过 Claude/Codex 对话集成。");
110
+ console.log("已跳过 Agent 对话集成。");
76
111
  return;
77
112
  }
78
113
  const harnessCliPath = path.resolve(process.argv[1]);
79
114
  const harnessCommand = `node ${quoteCommandPath(harnessCliPath)}`;
80
- const integration = await installIntegration({ targetDir: cwd, harnessCommand, harnessCliPath, agent: "both", packageRoot });
81
- console.log("Claude/Codex 对话集成已安装。");
115
+ const integration = await installIntegration({ targetDir: cwd, harnessCommand, harnessCliPath, agent: "all", packageRoot });
116
+ console.log("Claude/Codex/Cursor/Gemini 对话集成已安装。");
82
117
  console.log(`集成创建:${integration.created.length ? integration.created.join(", ") : "无"}`);
83
118
  console.log(`集成更新:${integration.updated.length ? integration.updated.join(", ") : "无"}`);
84
119
  console.log(`集成跳过:${integration.skipped.length ? integration.skipped.join(", ") : "无"}`);
@@ -87,6 +122,26 @@ function sayLive(live, message) {
87
122
  if (live)
88
123
  console.log(`[harness] ${message}`);
89
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
+ }
90
145
  async function commandRun(cwd, args) {
91
146
  const task = requireString(args, "task");
92
147
  const skillName = requireString(args, "skill");
@@ -121,24 +176,60 @@ async function commandRun(cwd, args) {
121
176
  await audit.event({ stage: "run", event: "run_finished", status: "failed" });
122
177
  throw new Error(`harness run 必须在 Git 仓库内执行。证据目录:${paths.runDir}`);
123
178
  }
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 = `# 执行阶段协议
179
+ const adapter = createAgentAdapter(agentName, config);
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({
213
+ cwd,
214
+ task,
215
+ config,
216
+ skill,
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 }
227
+ });
228
+ sayLive(live, `Context Pack 已生成:${iterationPaths.contextPack}`);
229
+ const plan = `# 执行阶段协议
230
+
231
+ ## 当前轮次
232
+ ${iteration}/${maxIterations}
142
233
 
143
234
  Agent 必须:
144
235
  1. 阅读 context_pack.md。
@@ -147,59 +238,123 @@ Agent 必须:
147
238
  4. 执行最小安全变更。
148
239
  5. 说明验证计划和残余风险。
149
240
  6. 让 Harness 运行验证并生成总结。
241
+ 7. 如果上一轮失败,只围绕失败原因做最小 rework。
150
242
  `;
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
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 });
248
+ await audit.event({
249
+ stage: "execute",
250
+ event: "agent_started",
251
+ status: "started",
252
+ detail: { agent: adapter.name, command: agentCommand, iteration }
168
253
  });
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 });
169
289
  await audit.event({
170
290
  stage: "execute",
171
- event: "agent_finished",
172
- status: agentResult.status === "success" ? "succeeded" : "failed",
173
- detail: { exitCode: agentResult.exitCode, changedFiles: agentResult.changedFiles, error: agentResult.error }
291
+ event: "agent_contract_check",
292
+ status: contractCheck.status === "passed" ? "succeeded" : "failed",
293
+ detail: { iteration, contractCheck }
174
294
  });
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)
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
185
339
  };
186
- 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}`);
187
357
  }
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
358
  await writeSummary({
204
359
  task,
205
360
  skillName,
@@ -214,7 +369,8 @@ Agent 必须:
214
369
  await audit.event({ stage: "persist", event: "suggestions_written", status: "succeeded" });
215
370
  sayLive(live, `总结已写入:${paths.summary}`);
216
371
  sayLive(live, `知识沉淀建议已写入:${paths.persistSuggestions}`);
217
- const runPassed = verifyResult.status === "passed" && agentResult.status === "success";
372
+ sayLive(live, `自纠偏报告已写入:${paths.loopReport}`);
373
+ const runPassed = evaluations.at(-1)?.status === "passed";
218
374
  await audit.event({ stage: "run", event: "run_finished", status: runPassed ? "succeeded" : "failed" });
219
375
  console.log(`运行完成:${paths.runId}`);
220
376
  console.log(`证据目录:${paths.runDir}`);
@@ -223,8 +379,8 @@ Agent 必须:
223
379
  }
224
380
  async function commandInstallIntegration(cwd, args) {
225
381
  const rawAgent = String(args.agent ?? "both");
226
- if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "both") {
227
- throw new Error(`无效 --agent ${rawAgent}。期望 claude、codex 或 both。`);
382
+ if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "cursor" && rawAgent !== "gemini" && rawAgent !== "both" && rawAgent !== "all") {
383
+ throw new Error(`无效 --agent ${rawAgent}。期望 claude、codex、cursor、gemini、bothall。`);
228
384
  }
229
385
  const targetDir = typeof args.target === "string" ? path.resolve(cwd, args.target) : cwd;
230
386
  const packageRoot = path.resolve(path.dirname(process.argv[1]), "..");
@@ -242,10 +398,48 @@ async function commandVerify(cwd, args) {
242
398
  const config = await loadConfig(cwd);
243
399
  const skill = await loadSkill(cwd, intake.skill);
244
400
  const result = await runVerify(cwd, config, skill, path.join(runDir, "verify.json"));
401
+ await writeVerifyDiagnostics(path.join(runDir, "verify_diagnostics.json"), result);
245
402
  const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
246
403
  await audit.event({ stage: "verify", event: "verify_rerun", status: result.status === "passed" ? "succeeded" : "failed", detail: result });
247
404
  console.log(`验证 ${result.status}:${runDir}`);
248
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 });
442
+ }
249
443
  async function commandSummarize(cwd, args) {
250
444
  const runDir = await resolveRunDir(cwd, requireString(args, "run"));
251
445
  const intake = JSON.parse(await readFile(path.join(runDir, "intake.json"), "utf8"));
@@ -286,12 +480,22 @@ async function main() {
286
480
  }
287
481
  if (command === "init")
288
482
  return commandInit(cwd);
483
+ if (command === "doctor")
484
+ return commandDoctor(cwd);
485
+ if (command === "smoke")
486
+ return commandSmoke(cwd, args);
289
487
  if (command === "run")
290
488
  return commandRun(cwd, args);
291
489
  if (command === "verify")
292
490
  return commandVerify(cwd, args);
293
491
  if (command === "summarize")
294
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);
295
499
  if (command === "install-integration")
296
500
  return commandInstallIntegration(cwd, args);
297
501
  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
@@ -0,0 +1,44 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ function hasValueAfter(label, output) {
3
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
+ const match = output.match(new RegExp(`${escaped}\\s*[::]?\\s*([^\\n]+)`, "i"));
5
+ if (!match)
6
+ return false;
7
+ const value = match[1].trim();
8
+ return Boolean(value && !/^[-(]?\s*(none|n\/a|unknown|无|没有|未使用|缺失|不适用)/i.test(value));
9
+ }
10
+ function hasAnyValueAfter(labels, output) {
11
+ return labels.some((label) => hasValueAfter(label, output));
12
+ }
13
+ function hasAnyPattern(patterns, output) {
14
+ return patterns.some((pattern) => pattern.test(output));
15
+ }
16
+ export function createContextScore(output) {
17
+ const requiredFields = {
18
+ codegraphToolsUsed: hasAnyValueAfter(["CodeGraph tools used", "CodeGraph 工具", "CodeGraph 使用工具", "代码图谱工具"], output) ||
19
+ hasAnyPattern([/codegraph[_-]?\w+/i, /codegraph_explore/i], output),
20
+ blastRadiusSummary: hasAnyValueAfter(["Blast-radius summary", "Blast radius summary", "影响范围", "影响面", "变更影响范围"], output) ||
21
+ hasAnyPattern([/blast[_ -]?radius/i, /影响范围\s*[::]\s*\S+/i], output),
22
+ knowledgeConceptIds: hasAnyValueAfter(["Candidate concept_ids", "Concept IDs", "候选 concept_ids", "知识库 concept_ids", "知识概念"], output) ||
23
+ hasAnyPattern([/concept[_ -]?ids?\s*[::]\s*\S+/i, /\/[^ \n]+\/(overview|features|metrics|api|playbooks|references)\//i], output),
24
+ missingContextDeclared: hasAnyValueAfter(["Missing context", "缺失上下文", "上下文缺口", "未确认上下文"], output) ||
25
+ hasAnyPattern([/Missing context\s*[::]/i, /缺失上下文\s*[::]/i, /上下文缺口\s*[::]/i], output)
26
+ };
27
+ const missing = Object.entries(requiredFields)
28
+ .filter(([, present]) => !present)
29
+ .map(([key]) => key);
30
+ const presentCount = Object.values(requiredFields).filter(Boolean).length;
31
+ return {
32
+ status: presentCount >= 4 ? "strong" : presentCount >= 2 ? "acceptable" : "weak",
33
+ requiredFields,
34
+ missing,
35
+ recommendation: missing.length
36
+ ? `下一轮请补充以下上下文证据:${missing.join(", ")}。`
37
+ : "上下文证据结构完整。"
38
+ };
39
+ }
40
+ export async function writeContextScore(path, output) {
41
+ const score = createContextScore(output);
42
+ await writeFile(path, `${JSON.stringify(score, null, 2)}\n`, "utf8");
43
+ return score;
44
+ }