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/COLLEAGUE_TRIAL_GUIDE.md +44 -16
- package/ONLINE_INSTALL_GUIDE.md +458 -0
- package/README.md +157 -22
- package/dist/agent.js +31 -19
- package/dist/approval.js +64 -0
- package/dist/audit.js +1 -0
- package/dist/checkpoint.js +83 -0
- package/dist/cli.js +285 -81
- package/dist/config.js +39 -1
- package/dist/context.js +14 -5
- package/dist/contextScore.js +44 -0
- package/dist/diagnostics.js +58 -0
- package/dist/doctor.js +100 -0
- package/dist/feedback.js +5 -0
- package/dist/integration.js +62 -2
- package/dist/loop.js +116 -0
- package/dist/mcp.js +1 -1
- package/dist/metrics.js +91 -0
- package/dist/replay.js +27 -0
- package/dist/runPaths.js +30 -0
- package/dist/summarize.js +139 -11
- package/package.json +3 -2
- package/scripts/read-evidence.ps1 +37 -1
- package/scripts/run-harness.ps1 +1 -1
- package/templates/default-project/TOOLS.md +6 -3
- package/templates/default-project/skills/add_feature/context.yaml +18 -15
- package/templates/default-project/skills/fix_bug/context.yaml +18 -15
- package/templates/default-project/skills/update_docs/context.yaml +16 -12
- package/templates/default-project/skills/write_tests/context.yaml +17 -14
- package/templates/integrations/claude/SKILL.md +47 -11
- package/templates/integrations/codex/AGENTS_BLOCK.md +42 -11
- package/templates/integrations/cursor/ai-dev-harness.mdc +55 -0
- package/templates/integrations/gemini/GEMINI_BLOCK.md +46 -0
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 {
|
|
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
|
|
39
|
-
harness
|
|
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
|
|
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 或
|
|
86
|
+
if (value !== "codex" && value !== "claude" && value !== "cursor" && value !== "gemini") {
|
|
87
|
+
throw new Error(`无效 agent:${value}。期望 codex、claude、cursor 或 gemini。`);
|
|
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("已跳过
|
|
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: "
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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: "
|
|
172
|
-
status:
|
|
173
|
-
detail: {
|
|
291
|
+
event: "agent_contract_check",
|
|
292
|
+
status: contractCheck.status === "passed" ? "succeeded" : "failed",
|
|
293
|
+
detail: { iteration, contractCheck }
|
|
174
294
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
-
|
|
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 或
|
|
382
|
+
if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "cursor" && rawAgent !== "gemini" && rawAgent !== "both" && rawAgent !== "all") {
|
|
383
|
+
throw new Error(`无效 --agent ${rawAgent}。期望 claude、codex、cursor、gemini、both 或 all。`);
|
|
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
|
|
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
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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
|
+
}
|