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