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.
Files changed (45) hide show
  1. package/COLLEAGUE_TRIAL_GUIDE.md +175 -0
  2. package/ONLINE_INSTALL_GUIDE.md +458 -0
  3. package/README.md +253 -54
  4. package/dist/agent.js +74 -49
  5. package/dist/approval.js +64 -0
  6. package/dist/audit.js +1 -0
  7. package/dist/checkpoint.js +83 -0
  8. package/dist/cli.js +326 -99
  9. package/dist/config.js +118 -9
  10. package/dist/context.js +62 -21
  11. package/dist/contextScore.js +44 -0
  12. package/dist/diagnostics.js +58 -0
  13. package/dist/doctor.js +100 -0
  14. package/dist/feedback.js +5 -0
  15. package/dist/init.js +8 -20
  16. package/dist/integration.js +84 -93
  17. package/dist/loop.js +116 -0
  18. package/dist/mcp.js +29 -112
  19. package/dist/metrics.js +91 -0
  20. package/dist/replay.js +27 -0
  21. package/dist/runPaths.js +31 -0
  22. package/dist/summarize.js +170 -39
  23. package/dist/templates.js +39 -215
  24. package/package.json +5 -2
  25. package/scripts/read-evidence.ps1 +45 -3
  26. package/scripts/run-harness.ps1 +9 -6
  27. package/templates/default-project/AGENTS.md +9 -0
  28. package/templates/default-project/SKILLS.md +13 -0
  29. package/templates/default-project/TOOLS.md +11 -0
  30. package/templates/default-project/skills/add_feature/SKILL.md +34 -0
  31. package/templates/default-project/skills/add_feature/context.yaml +33 -0
  32. package/templates/default-project/skills/add_feature/verify.yaml +2 -0
  33. package/templates/default-project/skills/fix_bug/SKILL.md +45 -0
  34. package/templates/default-project/skills/fix_bug/context.yaml +33 -0
  35. package/templates/default-project/skills/fix_bug/verify.yaml +2 -0
  36. package/templates/default-project/skills/update_docs/SKILL.md +28 -0
  37. package/templates/default-project/skills/update_docs/context.yaml +30 -0
  38. package/templates/default-project/skills/update_docs/verify.yaml +2 -0
  39. package/templates/default-project/skills/write_tests/SKILL.md +30 -0
  40. package/templates/default-project/skills/write_tests/context.yaml +32 -0
  41. package/templates/default-project/skills/write_tests/verify.yaml +2 -0
  42. package/templates/integrations/claude/SKILL.md +115 -0
  43. package/templates/integrations/codex/AGENTS_BLOCK.md +82 -0
  44. package/templates/integrations/cursor/ai-dev-harness.mdc +55 -0
  45. 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
- url: String(scalar(text, /mcp:\n[\s\S]*?codegraph:\n\s+url:\s*([^\n]+)/m, defaultConfig.mcp.codegraph.url))
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
- url: String(scalar(text, /mcp:\n[\s\S]*?knowledgeGraph:\n\s+url:\s*([^\n]+)/m, defaultConfig.mcp.knowledgeGraph.url))
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: "mcp_query", status: "started" });
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
- const degraded = codeContext.degraded || knowledgeContext.degraded;
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
- - Treat CodeGraph and Knowledge Graph results as primary context when status is ok.
70
- - If either MCP source is degraded, explicitly mention the degraded source in your analysis and rely on fallback context carefully.
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
- Status: ${codeContext.ok ? "ok" : "degraded"}
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
- Status: ${knowledgeContext.ok ? "ok" : "degraded"}
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, degraded, mcpResults: [codeContext, knowledgeContext] };
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
+ }
@@ -0,0 +1,5 @@
1
+ import { appendFile } from "node:fs/promises";
2
+ import { nowIso } from "./utils.js";
3
+ export async function feedbackEvent(feedbackPath, event, detail = {}) {
4
+ await appendFile(feedbackPath, `${JSON.stringify({ timestamp: nowIso(), event, ...detail })}\n`, "utf8");
5
+ }
package/dist/init.js CHANGED
@@ -1,29 +1,17 @@
1
1
  import path from "node:path";
2
- import { mkdir } from "node:fs/promises";
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 [skillName, files] of Object.entries(skillTemplates)) {
18
- await mkdir(path.join(cwd, "skills", skillName), { recursive: true });
19
- for (const [file, content] of Object.entries(files)) {
20
- const rel = path.join("skills", skillName, file);
21
- const target = path.join(cwd, rel);
22
- if (await writeFileIfMissing(target, content))
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
  }