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/COLLEAGUE_TRIAL_GUIDE.md +44 -16
- package/ONLINE_INSTALL_GUIDE.md +458 -0
- package/README.md +157 -22
- package/dist/agent.js +38 -19
- package/dist/approval.js +64 -0
- package/dist/audit.js +1 -0
- package/dist/checkpoint.js +83 -0
- package/dist/cli.js +322 -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,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
39
|
-
harness
|
|
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
|
|
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 或
|
|
118
|
+
if (value !== "codex" && value !== "claude" && value !== "cursor" && value !== "gemini") {
|
|
119
|
+
throw new Error(`无效 agent:${value}。期望 codex、claude、cursor 或 gemini。`);
|
|
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("已跳过
|
|
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: "
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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: "
|
|
172
|
-
status:
|
|
173
|
-
detail: {
|
|
324
|
+
event: "agent_contract_check",
|
|
325
|
+
status: contractCheck.status === "passed" ? "succeeded" : "failed",
|
|
326
|
+
detail: { iteration, contractCheck }
|
|
174
327
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
-
|
|
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 或
|
|
415
|
+
if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "cursor" && rawAgent !== "gemini" && rawAgent !== "both" && rawAgent !== "all") {
|
|
416
|
+
throw new Error(`无效 --agent ${rawAgent}。期望 claude、codex、cursor、gemini、both 或 all。`);
|
|
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
|
|
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
|