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
package/dist/config.js
CHANGED
|
@@ -4,11 +4,13 @@ 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
|
-
codegraph: { url: "http://localhost:7331/mcp" },
|
|
11
|
-
knowledgeGraph: { url: "http://localhost:7332/mcp" },
|
|
12
|
+
codegraph: { type: "http", url: "http://localhost:7331/mcp", headers: {} },
|
|
13
|
+
knowledgeGraph: { type: "http", url: "http://localhost:7332/mcp", headers: {} },
|
|
12
14
|
timeoutMs: 15000
|
|
13
15
|
},
|
|
14
16
|
verify: {
|
|
@@ -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) {
|
|
@@ -48,6 +65,8 @@ export function renderYaml(value, indent = 0) {
|
|
|
48
65
|
.join("\n");
|
|
49
66
|
}
|
|
50
67
|
if (typeof value === "object" && value !== null) {
|
|
68
|
+
if (Object.keys(value).length === 0)
|
|
69
|
+
return `${pad}{}`;
|
|
51
70
|
return Object.entries(value)
|
|
52
71
|
.map(([key, item]) => {
|
|
53
72
|
if (typeof item === "object" && item !== null)
|
|
@@ -65,7 +84,10 @@ function readScalar(raw) {
|
|
|
65
84
|
const value = raw.trim().replace(/^["']|["']$/g, "");
|
|
66
85
|
if (/^\d+$/.test(value))
|
|
67
86
|
return Number(value);
|
|
68
|
-
return value;
|
|
87
|
+
return expandEnv(value);
|
|
88
|
+
}
|
|
89
|
+
function expandEnv(value) {
|
|
90
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => process.env[name] ?? "");
|
|
69
91
|
}
|
|
70
92
|
function parseVerifyCommands(text) {
|
|
71
93
|
const commands = [];
|
|
@@ -95,14 +117,73 @@ function blockList(text, key) {
|
|
|
95
117
|
const match = text.match(new RegExp(`^\\s*${key}:\\n([\\s\\S]*?)(?=^\\s*[a-zA-Z][\\w]*:|^\\S|$)`, "m"));
|
|
96
118
|
return match ? parseStringList(match[1]) : [];
|
|
97
119
|
}
|
|
120
|
+
function mcpAskFor(text, source) {
|
|
121
|
+
const lines = text.split("\n");
|
|
122
|
+
const sourceIndex = lines.findIndex((line) => line === ` ${source}:`);
|
|
123
|
+
if (sourceIndex < 0)
|
|
124
|
+
return [];
|
|
125
|
+
const askForIndex = lines.findIndex((line, index) => index > sourceIndex && line === " ask_for:");
|
|
126
|
+
if (askForIndex < 0)
|
|
127
|
+
return [];
|
|
128
|
+
const values = [];
|
|
129
|
+
for (const line of lines.slice(askForIndex + 1)) {
|
|
130
|
+
if (!line.trim())
|
|
131
|
+
continue;
|
|
132
|
+
if (!line.startsWith(" - "))
|
|
133
|
+
break;
|
|
134
|
+
values.push(String(readScalar(line.slice(" - ".length))));
|
|
135
|
+
}
|
|
136
|
+
return values;
|
|
137
|
+
}
|
|
138
|
+
function objectBlock(text, key) {
|
|
139
|
+
const values = {};
|
|
140
|
+
const lines = text.split("\n");
|
|
141
|
+
const start = lines.findIndex((line) => line.match(new RegExp(`^\\s*${key}:\\s*$`)));
|
|
142
|
+
if (start < 0)
|
|
143
|
+
return values;
|
|
144
|
+
const baseIndent = lines[start].match(/^\s*/)?.[0].length ?? 0;
|
|
145
|
+
for (const line of lines.slice(start + 1)) {
|
|
146
|
+
if (!line.trim())
|
|
147
|
+
continue;
|
|
148
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
149
|
+
if (indent <= baseIndent)
|
|
150
|
+
break;
|
|
151
|
+
const item = line.match(/^\s*([A-Za-z0-9_-]+):\s*(.+)$/);
|
|
152
|
+
if (item)
|
|
153
|
+
values[item[1]] = String(readScalar(item[2]));
|
|
154
|
+
}
|
|
155
|
+
return values;
|
|
156
|
+
}
|
|
157
|
+
function indentedSection(text, key, indent) {
|
|
158
|
+
const lines = text.split("\n");
|
|
159
|
+
const start = lines.findIndex((line) => line.match(new RegExp(`^ {${indent}}${key}:\\s*$`)));
|
|
160
|
+
if (start < 0)
|
|
161
|
+
return "";
|
|
162
|
+
const collected = [];
|
|
163
|
+
for (const line of lines.slice(start + 1)) {
|
|
164
|
+
if (!line.trim()) {
|
|
165
|
+
collected.push(line);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const currentIndent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
169
|
+
if (currentIndent <= indent)
|
|
170
|
+
break;
|
|
171
|
+
collected.push(line);
|
|
172
|
+
}
|
|
173
|
+
return collected.join("\n");
|
|
174
|
+
}
|
|
98
175
|
export async function loadConfig(cwd) {
|
|
99
176
|
const configPath = path.join(cwd, "harness.yaml");
|
|
100
177
|
if (!(await pathExists(configPath)))
|
|
101
178
|
return defaultConfig;
|
|
102
|
-
const text = await readFile(configPath, "utf8");
|
|
179
|
+
const text = (await readFile(configPath, "utf8")).replace(/\r\n/g, "\n");
|
|
103
180
|
const verifyRoot = topSection(text, "verify");
|
|
104
181
|
const verifySection = verifyRoot.match(/defaultCommands:\n([\s\S]*?)(?=^\S|^ [a-zA-Z][\w]*:|$)/m)?.[1] ?? "";
|
|
105
182
|
const contextSection = topSection(text, "context");
|
|
183
|
+
const loopSection = topSection(text, "loop");
|
|
184
|
+
const policySection = topSection(text, "policy");
|
|
185
|
+
const codegraphSection = indentedSection(text, "codegraph", 2);
|
|
186
|
+
const knowledgeSection = indentedSection(text, "knowledgeGraph", 2);
|
|
106
187
|
return {
|
|
107
188
|
agents: {
|
|
108
189
|
codex: {
|
|
@@ -110,14 +191,24 @@ export async function loadConfig(cwd) {
|
|
|
110
191
|
},
|
|
111
192
|
claude: {
|
|
112
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))
|
|
113
200
|
}
|
|
114
201
|
},
|
|
115
202
|
mcp: {
|
|
116
203
|
codegraph: {
|
|
117
|
-
|
|
204
|
+
type: String(scalar(codegraphSection, /^\s*type:\s*([^\n]+)/m, defaultConfig.mcp.codegraph.type ?? "http")),
|
|
205
|
+
url: String(scalar(codegraphSection, /^\s*url:\s*([^\n]+)/m, defaultConfig.mcp.codegraph.url)),
|
|
206
|
+
headers: objectBlock(codegraphSection, "headers")
|
|
118
207
|
},
|
|
119
208
|
knowledgeGraph: {
|
|
120
|
-
|
|
209
|
+
type: String(scalar(knowledgeSection, /^\s*type:\s*([^\n]+)/m, defaultConfig.mcp.knowledgeGraph.type ?? "http")),
|
|
210
|
+
url: String(scalar(knowledgeSection, /^\s*url:\s*([^\n]+)/m, defaultConfig.mcp.knowledgeGraph.url)),
|
|
211
|
+
headers: objectBlock(knowledgeSection, "headers")
|
|
121
212
|
},
|
|
122
213
|
timeoutMs: Number(scalar(text, /mcp:\n[\s\S]*?timeoutMs:\s*([^\n]+)/m, defaultConfig.mcp.timeoutMs))
|
|
123
214
|
},
|
|
@@ -134,6 +225,19 @@ export async function loadConfig(cwd) {
|
|
|
134
225
|
exclude: blockList(contextSection, "exclude").length
|
|
135
226
|
? blockList(contextSection, "exclude")
|
|
136
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"
|
|
137
241
|
}
|
|
138
242
|
};
|
|
139
243
|
}
|
|
@@ -143,14 +247,19 @@ export async function loadSkill(cwd, name) {
|
|
|
143
247
|
throw new Error(`Skill not found: ${name}. Expected ${path.join(dir, "SKILL.md")}`);
|
|
144
248
|
}
|
|
145
249
|
const markdown = await readTextIfExists(path.join(dir, "SKILL.md"));
|
|
146
|
-
const verifyText = await readTextIfExists(path.join(dir, "verify.yaml"));
|
|
147
|
-
const contextText = await readTextIfExists(path.join(dir, "context.yaml"));
|
|
250
|
+
const verifyText = (await readTextIfExists(path.join(dir, "verify.yaml"))).replace(/\r\n/g, "\n");
|
|
251
|
+
const contextText = (await readTextIfExists(path.join(dir, "context.yaml"))).replace(/\r\n/g, "\n");
|
|
148
252
|
return {
|
|
149
253
|
name,
|
|
150
254
|
dir,
|
|
151
255
|
markdown,
|
|
152
256
|
verifyCommands: parseVerifyCommands(verifyText),
|
|
153
257
|
contextQueries: blockList(contextText, "queries"),
|
|
258
|
+
mcpPreflight: {
|
|
259
|
+
codegraph: mcpAskFor(contextText, "codegraph"),
|
|
260
|
+
knowledgeGraph: mcpAskFor(contextText, "knowledgeGraph")
|
|
261
|
+
},
|
|
262
|
+
stopConditions: blockList(contextText, "stop_conditions"),
|
|
154
263
|
fallbackInclude: blockList(contextText, "fallbackInclude")
|
|
155
264
|
};
|
|
156
265
|
}
|
package/dist/context.js
CHANGED
|
@@ -41,24 +41,15 @@ async function fallbackContext(cwd, config, skill) {
|
|
|
41
41
|
}
|
|
42
42
|
return snippets.join("\n\n");
|
|
43
43
|
}
|
|
44
|
+
function listSection(items, empty = "- (none configured)") {
|
|
45
|
+
return items.length ? items.map((item) => `- ${item}`).join("\n") : empty;
|
|
46
|
+
}
|
|
44
47
|
export async function buildContextPack(options) {
|
|
45
48
|
const { cwd, task, config, skill, contextPackPath, mcpLogPath, audit } = options;
|
|
46
|
-
await audit.event({ stage: "context_pack", event: "
|
|
49
|
+
await audit.event({ stage: "context_pack", event: "mcp_sources_record", status: "started" });
|
|
47
50
|
const source = new ContextSource(config, mcpLogPath);
|
|
48
51
|
const { codeContext, knowledgeContext } = await source.searchTaskContext(task, skill);
|
|
49
|
-
|
|
50
|
-
if (degraded)
|
|
51
|
-
await audit.event({
|
|
52
|
-
stage: "context_pack",
|
|
53
|
-
event: "mcp_query",
|
|
54
|
-
status: "degraded",
|
|
55
|
-
detail: {
|
|
56
|
-
codegraph: codeContext.error,
|
|
57
|
-
knowledgeGraph: knowledgeContext.error
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
else
|
|
61
|
-
await audit.event({ stage: "context_pack", event: "mcp_query", status: "succeeded" });
|
|
52
|
+
await audit.event({ stage: "context_pack", event: "mcp_sources_record", status: "succeeded" });
|
|
62
53
|
const branch = await git(cwd, "branch --show-current");
|
|
63
54
|
const status = await git(cwd, "status --short");
|
|
64
55
|
const recent = await git(cwd, "log -5 --oneline");
|
|
@@ -66,12 +57,62 @@ export async function buildContextPack(options) {
|
|
|
66
57
|
const markdown = `# Context Pack
|
|
67
58
|
|
|
68
59
|
## How To Use This Context
|
|
69
|
-
-
|
|
70
|
-
-
|
|
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.
|
|
63
|
+
- Harness records MCP configuration and audit evidence, but it does not implement MCP transport or replace the agent MCP client.
|
|
64
|
+
- If the current agent cannot access an MCP source, explicitly mention the missing source and rely on fallback context carefully.
|
|
71
65
|
- Do not assume fallback file snippets are complete repository knowledge.
|
|
72
66
|
- Cross-check task assumptions against Git state, project rules, and available source files before editing.
|
|
73
67
|
- Prefer minimal changes in files directly connected to the task.
|
|
74
68
|
|
|
69
|
+
## Agent Context Assembly Protocol
|
|
70
|
+
Before editing, assemble context in this exact shape:
|
|
71
|
+
|
|
72
|
+
### MCP Preflight Findings
|
|
73
|
+
- CodeGraph tools used:
|
|
74
|
+
- CodeGraph primary query:
|
|
75
|
+
- CodeGraph findings:
|
|
76
|
+
- Source grouped by file:
|
|
77
|
+
- Current line-numbered source:
|
|
78
|
+
- Call paths:
|
|
79
|
+
- Dynamic dispatch hops:
|
|
80
|
+
- Blast-radius summary:
|
|
81
|
+
- Knowledge Graph tools used:
|
|
82
|
+
- Knowledge Graph findings:
|
|
83
|
+
- Search tools used:
|
|
84
|
+
- Candidate concept_ids:
|
|
85
|
+
- Read tools used:
|
|
86
|
+
- Evidence sections/chunks:
|
|
87
|
+
- ADRs / decisions:
|
|
88
|
+
- Module notes:
|
|
89
|
+
- API notes:
|
|
90
|
+
- Runbooks / known issues:
|
|
91
|
+
- Audit/backlink findings:
|
|
92
|
+
- Missing context:
|
|
93
|
+
|
|
94
|
+
### Impact Analysis
|
|
95
|
+
- Root cause or implementation intent:
|
|
96
|
+
- Affected files/modules:
|
|
97
|
+
- User-visible behavior:
|
|
98
|
+
- Compatibility and risk:
|
|
99
|
+
|
|
100
|
+
### Proposed Plan
|
|
101
|
+
- Files to inspect:
|
|
102
|
+
- Files to change:
|
|
103
|
+
- Verification plan:
|
|
104
|
+
- Stop conditions checked:
|
|
105
|
+
|
|
106
|
+
## Skill MCP Preflight Requirements
|
|
107
|
+
### CodeGraph ask_for
|
|
108
|
+
${listSection(skill.mcpPreflight.codegraph)}
|
|
109
|
+
|
|
110
|
+
### Knowledge Graph ask_for
|
|
111
|
+
${listSection(skill.mcpPreflight.knowledgeGraph)}
|
|
112
|
+
|
|
113
|
+
## Skill Stop Conditions
|
|
114
|
+
${listSection(skill.stopConditions)}
|
|
115
|
+
|
|
75
116
|
## Task
|
|
76
117
|
${task}
|
|
77
118
|
|
|
@@ -92,15 +133,15 @@ ${status || "(clean or unavailable)"}
|
|
|
92
133
|
${recent || "(unavailable)"}
|
|
93
134
|
\`\`\`
|
|
94
135
|
|
|
95
|
-
## CodeGraph MCP
|
|
96
|
-
|
|
136
|
+
## CodeGraph MCP Source
|
|
137
|
+
Mode: agent-driven
|
|
97
138
|
|
|
98
139
|
\`\`\`json
|
|
99
140
|
${truncate(JSON.stringify(codeContext.data ?? { error: codeContext.error }, null, 2), 12000)}
|
|
100
141
|
\`\`\`
|
|
101
142
|
|
|
102
|
-
## Knowledge Graph MCP
|
|
103
|
-
|
|
143
|
+
## Knowledge Graph MCP Source
|
|
144
|
+
Mode: agent-driven
|
|
104
145
|
|
|
105
146
|
\`\`\`json
|
|
106
147
|
${truncate(JSON.stringify(knowledgeContext.data ?? { error: knowledgeContext.error }, null, 2), 12000)}
|
|
@@ -110,5 +151,5 @@ ${truncate(JSON.stringify(knowledgeContext.data ?? { error: knowledgeContext.err
|
|
|
110
151
|
${fallback || "(no fallback context found)"}
|
|
111
152
|
`;
|
|
112
153
|
await writeFile(contextPackPath, markdown, "utf8");
|
|
113
|
-
return { markdown,
|
|
154
|
+
return { markdown, mcpResults: [codeContext, knowledgeContext] };
|
|
114
155
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { truncate } from "./utils.js";
|
|
3
|
+
const FILE_PATTERN = /(?:^|\s)([A-Za-z]:[\\/][^\s:]+|\b[\w./\\-]+\.(?:ts|tsx|js|jsx|json|md|yaml|yml|py|java|cc|cpp|h|hpp|cs|go|rs))(?::(\d+))?/g;
|
|
4
|
+
function uniq(values) {
|
|
5
|
+
return [...new Set(values.filter(Boolean))].slice(0, 20);
|
|
6
|
+
}
|
|
7
|
+
function errorLines(log) {
|
|
8
|
+
return log
|
|
9
|
+
.split(/\r?\n/)
|
|
10
|
+
.filter((line) => /error|failed|failure|exception|assert|not ok|FAIL|Error:/i.test(line))
|
|
11
|
+
.slice(0, 30)
|
|
12
|
+
.map((line) => truncate(line, 500));
|
|
13
|
+
}
|
|
14
|
+
function suspectedFiles(log) {
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const match of log.matchAll(FILE_PATTERN))
|
|
17
|
+
files.push(match[1].replace(/\\/g, "/"));
|
|
18
|
+
return uniq(files);
|
|
19
|
+
}
|
|
20
|
+
function testNames(log) {
|
|
21
|
+
const names = [];
|
|
22
|
+
for (const line of log.split(/\r?\n/)) {
|
|
23
|
+
const subtest = line.match(/# Subtest:\s*(.+)$/);
|
|
24
|
+
if (subtest)
|
|
25
|
+
names.push(subtest[1].trim());
|
|
26
|
+
const jest = line.match(/(?:FAIL|✕|×)\s+(.+)$/);
|
|
27
|
+
if (jest)
|
|
28
|
+
names.push(jest[1].trim());
|
|
29
|
+
}
|
|
30
|
+
return uniq(names);
|
|
31
|
+
}
|
|
32
|
+
export function createVerifyDiagnostics(verify) {
|
|
33
|
+
const failedCommands = verify.commands
|
|
34
|
+
.filter((command) => command.exitCode !== 0)
|
|
35
|
+
.map((command) => {
|
|
36
|
+
const log = `${command.stdout}\n${command.stderr}`;
|
|
37
|
+
const errors = errorLines(log);
|
|
38
|
+
return {
|
|
39
|
+
name: command.name,
|
|
40
|
+
command: command.command,
|
|
41
|
+
exitCode: command.exitCode,
|
|
42
|
+
failureSummary: errors[0] ?? truncate(log.trim() || "验证命令失败但没有输出。", 1000),
|
|
43
|
+
errorLines: errors,
|
|
44
|
+
suspectedFiles: suspectedFiles(log),
|
|
45
|
+
testNames: testNames(log),
|
|
46
|
+
rawLogRef: "verify.json"
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
status: verify.status,
|
|
51
|
+
failedCommands
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function writeVerifyDiagnostics(path, verify) {
|
|
55
|
+
const diagnostics = createVerifyDiagnostics(verify);
|
|
56
|
+
await writeFile(path, `${JSON.stringify(diagnostics, null, 2)}\n`, "utf8");
|
|
57
|
+
return diagnostics;
|
|
58
|
+
}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { git, pathExists, runCommand } from "./utils.js";
|
|
3
|
+
const agentLabels = ["codex", "claude", "cursor", "gemini"];
|
|
4
|
+
function commandHead(command) {
|
|
5
|
+
const trimmed = command.trim();
|
|
6
|
+
const quoted = trimmed.match(/^"([^"]+)"/);
|
|
7
|
+
if (quoted)
|
|
8
|
+
return quoted[1];
|
|
9
|
+
return trimmed.split(/\s+/)[0] ?? "";
|
|
10
|
+
}
|
|
11
|
+
async function commandAvailable(command, cwd) {
|
|
12
|
+
const head = commandHead(command);
|
|
13
|
+
if (!head)
|
|
14
|
+
return false;
|
|
15
|
+
const probe = process.platform === "win32" ? `where ${head}` : `command -v ${head}`;
|
|
16
|
+
const result = await runCommand(probe, cwd, undefined, 5000);
|
|
17
|
+
return result.exitCode === 0;
|
|
18
|
+
}
|
|
19
|
+
function renderStatus(status) {
|
|
20
|
+
if (status === "ok")
|
|
21
|
+
return "OK";
|
|
22
|
+
if (status === "warn")
|
|
23
|
+
return "WARN";
|
|
24
|
+
return "FAIL";
|
|
25
|
+
}
|
|
26
|
+
export async function runDoctor(cwd, config) {
|
|
27
|
+
const checks = [];
|
|
28
|
+
const isGitRepo = Boolean(await git(cwd, "rev-parse --show-toplevel"));
|
|
29
|
+
checks.push({
|
|
30
|
+
name: "Git 仓库",
|
|
31
|
+
status: isGitRepo ? "ok" : "fail",
|
|
32
|
+
detail: isGitRepo ? "当前目录是 Git 仓库。" : "当前目录不是 Git 仓库,请在业务仓库根目录运行。"
|
|
33
|
+
});
|
|
34
|
+
const gitStatus = isGitRepo ? await git(cwd, "status --short") : "";
|
|
35
|
+
checks.push({
|
|
36
|
+
name: "Git 工作区",
|
|
37
|
+
status: gitStatus ? "warn" : "ok",
|
|
38
|
+
detail: gitStatus ? "当前存在未提交改动,restore 会拒绝自动恢复运行前已有改动。" : "当前工作区 clean。"
|
|
39
|
+
});
|
|
40
|
+
checks.push({
|
|
41
|
+
name: "harness.yaml",
|
|
42
|
+
status: (await pathExists(path.join(cwd, "harness.yaml"))) ? "ok" : "fail",
|
|
43
|
+
detail: (await pathExists(path.join(cwd, "harness.yaml"))) ? "配置文件存在。" : "未找到 harness.yaml,请先运行 harness init。"
|
|
44
|
+
});
|
|
45
|
+
const skillNames = ["fix_bug", "add_feature", "write_tests", "update_docs"];
|
|
46
|
+
const missingSkills = [];
|
|
47
|
+
for (const skill of skillNames) {
|
|
48
|
+
if (!(await pathExists(path.join(cwd, "skills", skill, "SKILL.md"))))
|
|
49
|
+
missingSkills.push(skill);
|
|
50
|
+
}
|
|
51
|
+
checks.push({
|
|
52
|
+
name: "内置 Skills",
|
|
53
|
+
status: missingSkills.length ? "fail" : "ok",
|
|
54
|
+
detail: missingSkills.length ? `缺少 Skill:${missingSkills.join(", ")}。` : "内置 Skill 文件完整。"
|
|
55
|
+
});
|
|
56
|
+
checks.push({
|
|
57
|
+
name: "对话集成脚本",
|
|
58
|
+
status: (await pathExists(path.join(cwd, ".harness", "ai-dev-harness", "scripts", "run-harness.ps1"))) ? "ok" : "warn",
|
|
59
|
+
detail: (await pathExists(path.join(cwd, ".harness", "ai-dev-harness", "scripts", "run-harness.ps1")))
|
|
60
|
+
? "run-harness.ps1 已安装。"
|
|
61
|
+
: "未找到对话集成脚本;CLI 仍可用,可运行 harness install-integration --agent all。"
|
|
62
|
+
});
|
|
63
|
+
for (const agent of agentLabels) {
|
|
64
|
+
const command = config.agents[agent].command;
|
|
65
|
+
const available = await commandAvailable(command, cwd);
|
|
66
|
+
checks.push({
|
|
67
|
+
name: `${agent} 执行器`,
|
|
68
|
+
status: available ? "ok" : "warn",
|
|
69
|
+
detail: available ? `命令可用:${command}` : `命令不可用或不在 PATH:${command}。不用该 agent 可忽略。`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const mcpUrls = [
|
|
73
|
+
["CodeGraph MCP", config.mcp.codegraph.url],
|
|
74
|
+
["Knowledge Graph MCP", config.mcp.knowledgeGraph.url]
|
|
75
|
+
];
|
|
76
|
+
for (const [name, url] of mcpUrls) {
|
|
77
|
+
const isDefaultLocal = /localhost:733[12]/.test(url);
|
|
78
|
+
checks.push({
|
|
79
|
+
name,
|
|
80
|
+
status: url && !isDefaultLocal ? "ok" : "warn",
|
|
81
|
+
detail: url
|
|
82
|
+
? isDefaultLocal
|
|
83
|
+
? `当前仍是默认地址:${url}。本地试点请改成真实 MCP source metadata。`
|
|
84
|
+
: `已配置地址:${url}。Harness 只记录 metadata,不主动连接。`
|
|
85
|
+
: "未配置 MCP 地址。"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const verifyCommands = config.verify.defaultCommands;
|
|
89
|
+
const onlyGitStatus = verifyCommands.length === 1 && verifyCommands[0].command === "git status --short";
|
|
90
|
+
checks.push({
|
|
91
|
+
name: "验证命令强度",
|
|
92
|
+
status: onlyGitStatus ? "warn" : "ok",
|
|
93
|
+
detail: onlyGitStatus
|
|
94
|
+
? "当前只有 git status,适合冒烟但验证较弱;试点真实任务建议补充 test/lint/typecheck。"
|
|
95
|
+
: `已配置 ${verifyCommands.length} 条默认验证命令。`
|
|
96
|
+
});
|
|
97
|
+
const failed = checks.some((check) => check.status === "fail");
|
|
98
|
+
const text = ["AI Dev Harness Doctor", ...checks.map((check) => `- [${renderStatus(check.status)}] ${check.name}: ${check.detail}`)].join("\n");
|
|
99
|
+
return { checks, failed, text };
|
|
100
|
+
}
|
package/dist/feedback.js
ADDED
package/dist/init.js
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { rootTemplates, skillTemplates } from "./templates.js";
|
|
2
|
+
import { defaultProjectTemplates, packageRootFromHere } from "./templates.js";
|
|
4
3
|
import { ensureDir, writeFileIfMissing } from "./utils.js";
|
|
5
|
-
export async function initHarness(cwd) {
|
|
4
|
+
export async function initHarness(cwd, packageRoot = packageRootFromHere()) {
|
|
6
5
|
const created = [];
|
|
7
6
|
const skipped = [];
|
|
8
|
-
for (const [file, content] of Object.entries(rootTemplates)) {
|
|
9
|
-
const target = path.join(cwd, file);
|
|
10
|
-
if (await writeFileIfMissing(target, content))
|
|
11
|
-
created.push(file);
|
|
12
|
-
else
|
|
13
|
-
skipped.push(file);
|
|
14
|
-
}
|
|
15
7
|
await ensureDir(path.join(cwd, "runs"));
|
|
16
8
|
await ensureDir(path.join(cwd, "skills"));
|
|
17
|
-
for (const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
created.push(rel);
|
|
24
|
-
else
|
|
25
|
-
skipped.push(rel);
|
|
26
|
-
}
|
|
9
|
+
for (const file of await defaultProjectTemplates(packageRoot)) {
|
|
10
|
+
const target = path.join(cwd, file.relPath);
|
|
11
|
+
if (await writeFileIfMissing(target, file.content))
|
|
12
|
+
created.push(file.relPath);
|
|
13
|
+
else
|
|
14
|
+
skipped.push(file.relPath);
|
|
27
15
|
}
|
|
28
16
|
return { created, skipped };
|
|
29
17
|
}
|