ai-dev-harness 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COLLEAGUE_TRIAL_GUIDE.md +44 -16
- package/ONLINE_INSTALL_GUIDE.md +458 -0
- package/README.md +157 -22
- package/dist/agent.js +31 -19
- package/dist/approval.js +64 -0
- package/dist/audit.js +1 -0
- package/dist/checkpoint.js +83 -0
- package/dist/cli.js +285 -81
- package/dist/config.js +39 -1
- package/dist/context.js +14 -5
- package/dist/contextScore.js +44 -0
- package/dist/diagnostics.js +58 -0
- package/dist/doctor.js +100 -0
- package/dist/feedback.js +5 -0
- package/dist/integration.js +62 -2
- package/dist/loop.js +116 -0
- package/dist/mcp.js +1 -1
- package/dist/metrics.js +91 -0
- package/dist/replay.js +27 -0
- package/dist/runPaths.js +30 -0
- package/dist/summarize.js +139 -11
- package/package.json +3 -2
- package/scripts/read-evidence.ps1 +37 -1
- package/scripts/run-harness.ps1 +1 -1
- package/templates/default-project/TOOLS.md +6 -3
- package/templates/default-project/skills/add_feature/context.yaml +18 -15
- package/templates/default-project/skills/fix_bug/context.yaml +18 -15
- package/templates/default-project/skills/update_docs/context.yaml +16 -12
- package/templates/default-project/skills/write_tests/context.yaml +17 -14
- package/templates/integrations/claude/SKILL.md +47 -11
- package/templates/integrations/codex/AGENTS_BLOCK.md +42 -11
- package/templates/integrations/cursor/ai-dev-harness.mdc +55 -0
- package/templates/integrations/gemini/GEMINI_BLOCK.md +46 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AI Dev Harness
|
|
2
2
|
|
|
3
|
-
AI Dev Harness 是一个可分发的 TypeScript CLI,用来把 Claude Code
|
|
3
|
+
AI Dev Harness 是一个可分发的 TypeScript CLI,用来把 Codex、Claude Code、Cursor Agent、Gemini CLI 的研发任务变成可审计的执行闭环。
|
|
4
4
|
|
|
5
5
|
它的核心定位不是替代 Agent,也不是实现 MCP Client,而是:
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ AI Dev Harness 是一个可分发的 TypeScript CLI,用来把 Claude Code / Co
|
|
|
8
8
|
|
|
9
9
|
## 核心边界
|
|
10
10
|
|
|
11
|
-
- Claude /
|
|
11
|
+
- Codex / Claude / Cursor / Gemini 负责:理解任务、调用 MCP、调用工具、使用 Skill、修改代码。
|
|
12
12
|
- Harness 负责:执行闭环、过程日志、验证命令、审计证据、总结、沉淀建议。
|
|
13
13
|
- CodeGraph / Knowledge Graph 是 Agent 侧 MCP 工具源,不是 Harness 内部服务依赖。
|
|
14
14
|
- Harness 会记录 MCP source metadata,但不会记录 bearer token 原文。
|
|
@@ -16,12 +16,13 @@ AI Dev Harness 是一个可分发的 TypeScript CLI,用来把 Claude Code / Co
|
|
|
16
16
|
## 包内容
|
|
17
17
|
|
|
18
18
|
- CLI:`dist/cli.js`
|
|
19
|
-
- Claude/Codex 对话集成安装器
|
|
19
|
+
- Claude/Codex/Cursor/Gemini 对话集成安装器
|
|
20
20
|
- Claude Skill:`.claude/skills/ai-dev-harness/`
|
|
21
21
|
- Agent 辅助脚本:`.harness/ai-dev-harness/scripts/`
|
|
22
22
|
- Git-native 项目技能:`skills/`
|
|
23
23
|
- 可编辑模板:`templates/`
|
|
24
24
|
|
|
25
|
+
线上安装使用手册见 `ONLINE_INSTALL_GUIDE.md`。
|
|
25
26
|
同事试用手册见 `COLLEAGUE_TRIAL_GUIDE.md`。
|
|
26
27
|
|
|
27
28
|
## 快速开始
|
|
@@ -30,7 +31,8 @@ AI Dev Harness 是一个可分发的 TypeScript CLI,用来把 Claude Code / Co
|
|
|
30
31
|
npm install
|
|
31
32
|
npm run build
|
|
32
33
|
node dist/cli.js init
|
|
33
|
-
node dist/cli.js
|
|
34
|
+
node dist/cli.js doctor
|
|
35
|
+
node dist/cli.js smoke --agent codex --live
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
CLI 应在业务 Git 仓库内初始化。`init` 会创建 `harness.yaml`、`skills/`、`AGENTS.md`、`TOOLS.md`、`SKILLS.md` 和 `runs/`。
|
|
@@ -40,29 +42,39 @@ CLI 应在业务 Git 仓库内初始化。`init` 会创建 `harness.yaml`、`ski
|
|
|
40
42
|
```bash
|
|
41
43
|
node dist/cli.js init
|
|
42
44
|
node dist/cli.js init --no-integration
|
|
45
|
+
node dist/cli.js doctor
|
|
46
|
+
node dist/cli.js smoke --agent claude --live
|
|
43
47
|
node dist/cli.js run --task "add refund flow tests" --skill write_tests --agent claude
|
|
44
48
|
node dist/cli.js run --task "analyze download startup flow. Do not modify repository files." --skill update_docs --agent claude --live
|
|
49
|
+
node dist/cli.js run --task "fix a small bug" --skill fix_bug --agent cursor --live
|
|
50
|
+
node dist/cli.js run --task "fix a small bug" --skill fix_bug --agent gemini --live
|
|
45
51
|
node dist/cli.js verify --run <run_id_or_path>
|
|
46
52
|
node dist/cli.js summarize --run <run_id_or_path>
|
|
47
|
-
node dist/cli.js
|
|
53
|
+
node dist/cli.js restore --run <run_id_or_path> --iteration 1
|
|
54
|
+
node dist/cli.js runs metrics --json
|
|
55
|
+
node dist/cli.js replay --run <run_id_or_path> --dry
|
|
56
|
+
node dist/cli.js replay --run <run_id_or_path> --agent codex
|
|
57
|
+
node dist/cli.js install-integration --agent all --target <repo>
|
|
48
58
|
```
|
|
49
59
|
|
|
50
|
-
## Claude/Codex 对话触发
|
|
60
|
+
## Claude/Codex/Cursor/Gemini 对话触发
|
|
51
61
|
|
|
52
62
|
`harness init` 默认安装对话集成。也可以手动刷新:
|
|
53
63
|
|
|
54
64
|
```bash
|
|
55
|
-
node "E:\ai native\dist\cli.js" install-integration --agent
|
|
65
|
+
node "E:\ai native\dist\cli.js" install-integration --agent all --target "C:\path\to\repo"
|
|
56
66
|
```
|
|
57
67
|
|
|
58
68
|
它会创建:
|
|
59
69
|
|
|
60
70
|
- `.claude/skills/ai-dev-harness/SKILL.md`
|
|
61
71
|
- `AGENTS.md` 中的 Harness 规则块
|
|
72
|
+
- `.cursor/rules/ai-dev-harness.mdc`
|
|
73
|
+
- `GEMINI.md` 中的 Harness 规则块
|
|
62
74
|
- `.harness/ai-dev-harness/scripts/run-harness.ps1`
|
|
63
75
|
- `.harness/ai-dev-harness/scripts/read-evidence.ps1`
|
|
64
76
|
|
|
65
|
-
然后在 Claude Code 或
|
|
77
|
+
然后在 Claude Code、Codex、Cursor 或 Gemini 中自然提问:
|
|
66
78
|
|
|
67
79
|
```text
|
|
68
80
|
Use AI Dev Harness to run update_docs: analyze the download startup flow. Do not modify repository files.
|
|
@@ -80,22 +92,49 @@ Use Harness to run fix_bug: fix the download resume progress issue.
|
|
|
80
92
|
|
|
81
93
|
Harness 不直接调用 MCP。Agent 应在触发 Harness 前使用自己已配置的 MCP 工具,并按以下格式汇报:
|
|
82
94
|
|
|
95
|
+
如果 CodeGraph 使用的是 `colbymchenry/codegraph`,默认优先调用 `codegraph_explore`。用任务里的业务流程、文件路径、函数/类/符号名或错误现象作为 query,并重点读取它返回的 grouped source、line-numbered source、call paths、dynamic dispatch hops 和 blast-radius summary。
|
|
96
|
+
|
|
97
|
+
如果 Knowledge Graph 使用的是 `browser_knowledge_service`,推荐读取顺序:
|
|
98
|
+
|
|
99
|
+
1. 用 `list_path`、`search_docs`、`search_sections`、`search_chunks` 缩小范围。
|
|
100
|
+
2. RAG 场景优先用 `answer_context`;需要自控排序时用 `hybrid_search`。
|
|
101
|
+
3. 定位 concept_id 后先用 `read_doc_outline` 看结构。
|
|
102
|
+
4. 再用 `read_section`、`read_doc_range`、`read_chunk`、`get_related_chunks` 精读证据。
|
|
103
|
+
5. 需要关系时用 `get_backlinks`,需要近期变更记录时用 `get_audit_log`。
|
|
104
|
+
6. v1 不直接调用 `propose_doc` 写入知识库;只生成 `persist_suggestions.md`。
|
|
105
|
+
|
|
83
106
|
```text
|
|
84
107
|
## MCP Preflight Findings
|
|
85
108
|
- CodeGraph tools used:
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
109
|
+
- CodeGraph primary query:
|
|
110
|
+
- Source grouped by file:
|
|
111
|
+
- Current line-numbered source:
|
|
112
|
+
- Call paths:
|
|
113
|
+
- Dynamic dispatch hops:
|
|
114
|
+
- Blast-radius summary:
|
|
90
115
|
- Knowledge Graph tools used:
|
|
116
|
+
- Candidate concept_ids:
|
|
117
|
+
- Read tools used:
|
|
118
|
+
- Evidence sections/chunks:
|
|
91
119
|
- ADRs / decisions:
|
|
92
120
|
- Module notes:
|
|
93
121
|
- API notes / runbooks:
|
|
122
|
+
- Audit/backlink findings:
|
|
94
123
|
- Missing context:
|
|
95
124
|
```
|
|
96
125
|
|
|
97
126
|
关键 MCP 发现应被带入 Harness 的任务文本,进入审计证据。
|
|
98
127
|
|
|
128
|
+
## OKF 知识沉淀
|
|
129
|
+
|
|
130
|
+
`persist_suggestions.md` 按 PC 浏览器团队知识库规范生成 OKF 候选内容,不自动写入 Knowledge Graph。
|
|
131
|
+
|
|
132
|
+
- 候选文档路径应落到 `/<project>/overview/`、`/<project>/features/`、`/<project>/metrics/`、`/<project>/api/`、`/<project>/playbooks/` 或 `/<project>/references/`。
|
|
133
|
+
- 每个候选 concept 应包含 `type`、`title`、`description`、`tags`、`timestamp`;API/数据类建议补充 `resource`。
|
|
134
|
+
- `## 相关` 必须使用有效 Markdown 链接,推荐 bundle 根相对路径。
|
|
135
|
+
- `type: API Reference` 需要包含 `## 关键实现锚点` 和 `### Agent 使用建议`。
|
|
136
|
+
- 合入知识库前应同步更新对应目录的 `index.md` 和 `log.md`,并人工检查敏感信息。
|
|
137
|
+
|
|
99
138
|
## 配置
|
|
100
139
|
|
|
101
140
|
编辑业务仓库里的 `harness.yaml`:
|
|
@@ -106,6 +145,10 @@ agents:
|
|
|
106
145
|
command: codex exec --full-auto -
|
|
107
146
|
claude:
|
|
108
147
|
command: claude -p -
|
|
148
|
+
cursor:
|
|
149
|
+
command: cursor-agent
|
|
150
|
+
gemini:
|
|
151
|
+
command: gemini
|
|
109
152
|
mcp:
|
|
110
153
|
codegraph:
|
|
111
154
|
type: sse
|
|
@@ -123,10 +166,55 @@ verify:
|
|
|
123
166
|
command: git status --short
|
|
124
167
|
audit:
|
|
125
168
|
path: runs
|
|
169
|
+
loop:
|
|
170
|
+
maxIterations: 1
|
|
171
|
+
stopOn:
|
|
172
|
+
- same_failure_repeated
|
|
173
|
+
- max_iterations_reached
|
|
174
|
+
policy:
|
|
175
|
+
protectedPaths:
|
|
176
|
+
- .github/
|
|
177
|
+
- scripts/release/
|
|
178
|
+
- security/
|
|
179
|
+
requireApproval:
|
|
180
|
+
- protected_path_change
|
|
181
|
+
- dependency_change
|
|
182
|
+
- delete_files
|
|
183
|
+
- public_api_change_declared
|
|
184
|
+
- security_change_declared
|
|
185
|
+
approvalMode: file
|
|
126
186
|
```
|
|
127
187
|
|
|
128
188
|
MCP header 支持环境变量,例如 `${OKF_TOKEN}`。不要把真实 token 提交到 Git。
|
|
129
189
|
|
|
190
|
+
`agents.*.command` 都可以按团队机器上的实际 CLI 改写。Harness 统一通过 stdin 把最终 prompt 传给执行器,不绑定 Codex、Claude、Cursor 或 Gemini 的内部能力。
|
|
191
|
+
|
|
192
|
+
## 自纠偏循环
|
|
193
|
+
|
|
194
|
+
Harness 会在每轮执行后进入 `Evaluate` 阶段,判断是否完成目标、Agent 契约是否满足、验证是否通过。
|
|
195
|
+
|
|
196
|
+
默认 `loop.maxIterations: 1`,行为等同单轮执行。需要开启低风险自动重做时,在业务仓库 `harness.yaml` 中调大:
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
loop:
|
|
200
|
+
maxIterations: 2
|
|
201
|
+
stopOn:
|
|
202
|
+
- same_failure_repeated
|
|
203
|
+
- max_iterations_reached
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
会自动进入下一轮的典型情况:
|
|
207
|
+
|
|
208
|
+
- Agent 命令失败。
|
|
209
|
+
- Agent 输出缺少 `Impact Analysis / Plan / Execution Notes / Verification Notes / Final Report`。
|
|
210
|
+
- 验证命令失败。
|
|
211
|
+
|
|
212
|
+
会停止并要求人工评审的典型情况:
|
|
213
|
+
|
|
214
|
+
- 连续两轮出现同一种失败。
|
|
215
|
+
- 达到最大循环轮次。
|
|
216
|
+
- Agent 在报告里说明目标不明确、需要扩大范围、要改公共接口或安全策略。
|
|
217
|
+
|
|
130
218
|
## 运行证据
|
|
131
219
|
|
|
132
220
|
每次 `run` 会创建 `runs/<run_id>/`:
|
|
@@ -134,12 +222,20 @@ MCP header 支持环境变量,例如 `${OKF_TOKEN}`。不要把真实 token
|
|
|
134
222
|
- `intake.json`:任务、skill、agent、Git 状态。
|
|
135
223
|
- `mcp_queries.jsonl`:Agent-driven MCP source metadata,不包含 token 原文。
|
|
136
224
|
- `context_pack.md`:上下文组装协议、MCP source 记录、本地兜底上下文。
|
|
137
|
-
- `agent_prompt.md
|
|
225
|
+
- `agent_prompt.md`:发送给当前执行器的最终提示词。
|
|
138
226
|
- `plan.md`:Harness 执行阶段协议。
|
|
139
227
|
- `execution.log`:Agent stdout/stderr。
|
|
140
228
|
- `agent_contract.json`:Agent 必需章节检查结果。
|
|
141
229
|
- `git_diff.patch`:代码变更。
|
|
142
230
|
- `verify.json`:验证结果。
|
|
231
|
+
- `verify_diagnostics.json`:从验证 stdout/stderr 提取的通用失败诊断。
|
|
232
|
+
- `context_score.json`:Agent 是否按协议提供上下文证据的结构化评分。
|
|
233
|
+
- `approval_required.json`:机械审批门禁结果。
|
|
234
|
+
- `feedback.jsonl`:Harness 可见机器反馈事件,不记录 Agent 内部工具调用。
|
|
235
|
+
- `checkpoint.json`:本轮 Git checkpoint 和 restore 支持状态。
|
|
236
|
+
- `pre_iteration_diff.patch` / `post_iteration_diff.patch`:本轮前后 diff。
|
|
237
|
+
- `loop_review.json`:每轮评估、失败归因和下一步动作。
|
|
238
|
+
- `loop_report.md`:自纠偏循环的人类可读报告。
|
|
143
239
|
- `summary.md`:评审/PR 总结。
|
|
144
240
|
- `persist_suggestions.md`:知识沉淀建议。
|
|
145
241
|
- `audit.jsonl`:完整审计事件流。
|
|
@@ -154,6 +250,43 @@ Harness 会检查 Agent 输出是否包含以下章节:
|
|
|
154
250
|
|
|
155
251
|
检查结果写入 `agent_contract.json`,并记录到 `audit.jsonl`。
|
|
156
252
|
|
|
253
|
+
每轮循环会额外生成 `iteration-<n>/` 子目录。根目录同名文件是最后一轮快照,方便旧脚本继续读取。
|
|
254
|
+
|
|
255
|
+
## Checkpoint / Restore
|
|
256
|
+
|
|
257
|
+
Harness 每轮开始记录 checkpoint。只有运行开始前工作区是 clean,才允许自动 restore:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
node dist/cli.js restore --run <run_id_or_path> --iteration 1
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
如果运行前已有未提交改动,Harness 会拒绝自动恢复,避免覆盖用户手工改动。
|
|
264
|
+
|
|
265
|
+
## Approval Gate
|
|
266
|
+
|
|
267
|
+
Harness 只做机械门禁,不判断业务方案。默认会在以下情况写入 `approval_required.json` 并停止:
|
|
268
|
+
|
|
269
|
+
- 命中 `policy.protectedPaths`。
|
|
270
|
+
- 修改依赖文件,例如 `package.json`、lockfile。
|
|
271
|
+
- 删除文件。
|
|
272
|
+
- Agent 最终报告声明 `public_api_change_declared` 或 `security_change_declared`。
|
|
273
|
+
|
|
274
|
+
## Metrics / Replay
|
|
275
|
+
|
|
276
|
+
读取本地 `runs/` 证据生成运行指标:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
node dist/cli.js runs metrics
|
|
280
|
+
node dist/cli.js runs metrics --json
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Replay 复用历史 run 的 task 和 skill,创建新的 run,不覆盖旧证据:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
node dist/cli.js replay --run <run_id_or_path> --dry
|
|
287
|
+
node dist/cli.js replay --run <run_id_or_path> --agent claude
|
|
288
|
+
```
|
|
289
|
+
|
|
157
290
|
## Skill 规范
|
|
158
291
|
|
|
159
292
|
内置 skill 会通过 `context.yaml` 告诉 Agent:
|
|
@@ -169,19 +302,21 @@ Harness 会检查 Agent 输出是否包含以下章节:
|
|
|
169
302
|
mcp_preflight:
|
|
170
303
|
codegraph:
|
|
171
304
|
required: true
|
|
305
|
+
primary_tool: codegraph_explore
|
|
172
306
|
ask_for:
|
|
173
|
-
-
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
177
|
-
-
|
|
307
|
+
- relevant_source_grouped_by_file
|
|
308
|
+
- current_line_numbered_source_for_known_file_or_symbol
|
|
309
|
+
- call_paths_between_related_symbols
|
|
310
|
+
- dynamic_dispatch_hops
|
|
311
|
+
- blast_radius_summary
|
|
178
312
|
knowledgeGraph:
|
|
179
313
|
required: true
|
|
180
314
|
ask_for:
|
|
181
|
-
-
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
-
|
|
315
|
+
- search_docs_or_search_chunks_for_defect_terms
|
|
316
|
+
- answer_context_for_direct_rag_context
|
|
317
|
+
- read_doc_outline_for_relevant_concepts
|
|
318
|
+
- read_section_or_read_doc_range_for_evidence
|
|
319
|
+
- get_backlinks_for_related_decisions
|
|
185
320
|
stop_conditions:
|
|
186
321
|
- root cause is unclear
|
|
187
322
|
- impact scope cannot be determined
|
package/dist/agent.js
CHANGED
|
@@ -3,6 +3,9 @@ import { git, runCommand, truncate } from "./utils.js";
|
|
|
3
3
|
function renderPrompt(input, contextPack) {
|
|
4
4
|
return `你正在一个可审计的 AI 研发 Harness 中执行任务。请把它当作公司级研发任务,而不是随意聊天。
|
|
5
5
|
|
|
6
|
+
当前循环轮次:
|
|
7
|
+
${input.iteration ?? 1}
|
|
8
|
+
|
|
6
9
|
任务:
|
|
7
10
|
${input.task}
|
|
8
11
|
|
|
@@ -31,7 +34,14 @@ ${input.skill.name}
|
|
|
31
34
|
- 修改文件。
|
|
32
35
|
- 已执行或延后的验证。
|
|
33
36
|
- 残余风险。
|
|
34
|
-
-
|
|
37
|
+
- 知识沉淀建议。建议必须遵循 OKF v0.1 扩展:说明建议写入的项目目录、子域、type、title、description、相关 concept 链接、index.md/log.md 更新项;API 文档还要说明关键实现锚点和 Agent 使用建议。
|
|
38
|
+
|
|
39
|
+
自纠偏规则:
|
|
40
|
+
- 如果这是第 2 轮或更高轮次,先阅读上一轮失败诊断,只围绕失败原因做最小 rework。
|
|
41
|
+
- 不要因为验证失败就绕过验证或删除验证。
|
|
42
|
+
- 如果需要扩大范围、改公共接口、修改安全策略、或目标本身不明确,请停止并在 Final Report 中说明需要人工确认。
|
|
43
|
+
|
|
44
|
+
${input.previousFeedback ? `上一轮反馈:\n${input.previousFeedback}\n` : ""}
|
|
35
45
|
|
|
36
46
|
必须使用以下输出格式,标题不要改名:
|
|
37
47
|
## Impact Analysis
|
|
@@ -62,6 +72,19 @@ export function checkAgentContract(output) {
|
|
|
62
72
|
missingSections
|
|
63
73
|
};
|
|
64
74
|
}
|
|
75
|
+
function parseChangedFiles(status) {
|
|
76
|
+
return status
|
|
77
|
+
.split(/\r?\n/)
|
|
78
|
+
.map((line) => line.trim())
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map((line) => {
|
|
81
|
+
const rename = line.match(/^R\s+(.+)\s+->\s+(.+)$/);
|
|
82
|
+
if (rename)
|
|
83
|
+
return rename[2];
|
|
84
|
+
return line.slice(3).trim();
|
|
85
|
+
})
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
}
|
|
65
88
|
async function executeAgent(input) {
|
|
66
89
|
const contextPack = await readFile(input.contextPackPath, "utf8");
|
|
67
90
|
const prompt = renderPrompt(input, contextPack);
|
|
@@ -73,9 +96,9 @@ async function executeAgent(input) {
|
|
|
73
96
|
stderrPrefix: `[${input.agent}:stderr] `
|
|
74
97
|
});
|
|
75
98
|
await appendFile(input.executionLogPath, `## STDOUT\n${result.stdout}\n\n## STDERR\n${result.stderr}\n`, "utf8");
|
|
76
|
-
const changedFilesRaw = await git(input.cwd, "
|
|
99
|
+
const changedFilesRaw = await git(input.cwd, "status --porcelain");
|
|
77
100
|
const diff = await git(input.cwd, "diff --no-ext-diff");
|
|
78
|
-
const changedFiles = changedFilesRaw ? changedFilesRaw
|
|
101
|
+
const changedFiles = changedFilesRaw ? parseChangedFiles(changedFilesRaw) : [];
|
|
79
102
|
return {
|
|
80
103
|
status: result.exitCode === 0 ? "success" : "failed",
|
|
81
104
|
exitCode: result.exitCode,
|
|
@@ -86,20 +109,11 @@ async function executeAgent(input) {
|
|
|
86
109
|
error: result.exitCode === 0 ? undefined : `Agent command failed with exit code ${result.exitCode}`
|
|
87
110
|
};
|
|
88
111
|
}
|
|
89
|
-
export class
|
|
90
|
-
|
|
91
|
-
name = "codex";
|
|
92
|
-
constructor(command) {
|
|
93
|
-
this.command = command;
|
|
94
|
-
}
|
|
95
|
-
run(input) {
|
|
96
|
-
return executeAgent({ ...input, agent: this.name, command: this.command });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
export class ClaudeAdapter {
|
|
112
|
+
export class CliAgentAdapter {
|
|
113
|
+
name;
|
|
100
114
|
command;
|
|
101
|
-
name
|
|
102
|
-
|
|
115
|
+
constructor(name, command) {
|
|
116
|
+
this.name = name;
|
|
103
117
|
this.command = command;
|
|
104
118
|
}
|
|
105
119
|
run(input) {
|
|
@@ -107,7 +121,5 @@ export class ClaudeAdapter {
|
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
export function createAgentAdapter(agent, config) {
|
|
110
|
-
|
|
111
|
-
return new CodexAdapter(config.agents.codex.command);
|
|
112
|
-
return new ClaudeAdapter(config.agents.claude.command);
|
|
124
|
+
return new CliAgentAdapter(agent, config.agents[agent].command);
|
|
113
125
|
}
|
package/dist/approval.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { git } from "./utils.js";
|
|
4
|
+
function normalizeRel(file) {
|
|
5
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6
|
+
}
|
|
7
|
+
function matchesProtected(file, protectedPath) {
|
|
8
|
+
const normalized = normalizeRel(file);
|
|
9
|
+
const protectedNormalized = normalizeRel(protectedPath).replace(/\/?$/, "/");
|
|
10
|
+
return normalized === protectedNormalized.slice(0, -1) || normalized.startsWith(protectedNormalized);
|
|
11
|
+
}
|
|
12
|
+
function dependencyFile(file) {
|
|
13
|
+
const base = path.basename(file).toLowerCase();
|
|
14
|
+
return ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "requirements.txt", "poetry.lock", "go.mod", "go.sum", "cargo.toml", "cargo.lock"].includes(base);
|
|
15
|
+
}
|
|
16
|
+
function declaredRisk(output, marker) {
|
|
17
|
+
return new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(output);
|
|
18
|
+
}
|
|
19
|
+
function deletedFilesFromNameStatus(nameStatus) {
|
|
20
|
+
return nameStatus
|
|
21
|
+
.split(/\r?\n/)
|
|
22
|
+
.filter((line) => line.startsWith("D\t"))
|
|
23
|
+
.map((line) => normalizeRel(line.slice(2)));
|
|
24
|
+
}
|
|
25
|
+
export async function evaluateApprovalGate(options) {
|
|
26
|
+
const changedFiles = options.agentResult.changedFiles.map(normalizeRel);
|
|
27
|
+
const unstagedNameStatus = await git(options.cwd, "diff --name-status");
|
|
28
|
+
const stagedNameStatus = await git(options.cwd, "diff --cached --name-status");
|
|
29
|
+
const deleted = Array.from(new Set([...deletedFilesFromNameStatus(unstagedNameStatus), ...deletedFilesFromNameStatus(stagedNameStatus)]));
|
|
30
|
+
const reasons = [];
|
|
31
|
+
const required = new Set(options.config.policy.requireApproval);
|
|
32
|
+
if (required.has("protected_path_change")) {
|
|
33
|
+
for (const file of changedFiles) {
|
|
34
|
+
const matched = options.config.policy.protectedPaths.find((protectedPath) => matchesProtected(file, protectedPath));
|
|
35
|
+
if (matched)
|
|
36
|
+
reasons.push({ type: "protected_path_change", detail: `${file} matches ${matched}` });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (required.has("dependency_change")) {
|
|
40
|
+
for (const file of changedFiles.filter(dependencyFile))
|
|
41
|
+
reasons.push({ type: "dependency_change", detail: file });
|
|
42
|
+
}
|
|
43
|
+
if (required.has("delete_files")) {
|
|
44
|
+
for (const file of deleted)
|
|
45
|
+
reasons.push({ type: "delete_files", detail: file });
|
|
46
|
+
}
|
|
47
|
+
if (required.has("public_api_change_declared") && declaredRisk(options.agentOutput, "public_api_change_declared")) {
|
|
48
|
+
reasons.push({ type: "public_api_change_declared", detail: "Agent declared public API change." });
|
|
49
|
+
}
|
|
50
|
+
if (required.has("security_change_declared") && declaredRisk(options.agentOutput, "security_change_declared")) {
|
|
51
|
+
reasons.push({ type: "security_change_declared", detail: "Agent declared security change." });
|
|
52
|
+
}
|
|
53
|
+
const approval = {
|
|
54
|
+
required: reasons.length > 0,
|
|
55
|
+
reasons,
|
|
56
|
+
changedFiles,
|
|
57
|
+
mode: "file",
|
|
58
|
+
message: reasons.length
|
|
59
|
+
? "Harness 已停止:本次变更触发审批门禁,请人工评审 approval_required.json。"
|
|
60
|
+
: "未触发审批门禁。"
|
|
61
|
+
};
|
|
62
|
+
await writeFile(options.approvalPath, `${JSON.stringify(approval, null, 2)}\n`, "utf8");
|
|
63
|
+
return approval;
|
|
64
|
+
}
|
package/dist/audit.js
CHANGED
|
@@ -17,5 +17,6 @@ export async function prepareRunFiles(paths) {
|
|
|
17
17
|
await ensureDir(paths.runDir);
|
|
18
18
|
await writeJson(paths.intake, {});
|
|
19
19
|
await writeFile(paths.mcpQueries, "", "utf8");
|
|
20
|
+
await writeFile(paths.feedback, "", "utf8");
|
|
20
21
|
await writeFile(paths.audit, "", "utf8");
|
|
21
22
|
}
|
|
@@ -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
|
+
}
|