@erzhe/harness-kit 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/README.md +98 -37
- package/SPEC-v0.md +3 -1
- package/dist/harness-kit.cjs +164 -29
- package/package.json +3 -2
- package/skills/erzhe-harness-init/SKILL.md +34 -5
package/README.md
CHANGED
|
@@ -1,70 +1,131 @@
|
|
|
1
1
|
# @erzhe/harness-kit
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 给 Agent 发一句话,让它帮你把仓库变成 AI 友好的。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
harness-kit 把仓库的工程知识(这是什么、怎么跑、什么不能破、改东西看哪)沉淀成一份 `.agents/manifest.yaml`,然后自动生成各家 Agent 的入口文件(`AGENTS.md` / `CLAUDE.md` / Cursor rules),并用门禁保证它们始终一致。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 最快上手
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
在**任意仓库**里,把这句话丢给你的 Agent(Cursor / Claude Code / Codex 通用):
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm i -g @erzhe/harness-kit # 或 pnpm add -g / 直接 npx @erzhe/harness-kit
|
|
17
|
-
harness-kit --help
|
|
13
|
+
```
|
|
14
|
+
Onboard 本仓库到 harness-kit:跑 npx -y @erzhe/harness-kit@latest onboard,然后严格按输出执行。
|
|
18
15
|
```
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
就这一句。Agent 会:
|
|
18
|
+
|
|
19
|
+
1. 拉取最新版的 onboard skill
|
|
20
|
+
2. 扫描你的仓库(读 README、package.json、目录结构…)
|
|
21
|
+
3. 逐块跟你确认着填 `.agents/manifest.yaml`
|
|
22
|
+
4. 跑 `sync` / `doctor` / `verify` 直到全绿
|
|
23
|
+
|
|
24
|
+
全程走 `npx`,不往机器上装任何全局东西;你一发新版,所有人下次执行就用上了。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 初始化后你得到什么
|
|
21
29
|
|
|
22
30
|
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
你的仓库/
|
|
32
|
+
AGENTS.md ← [生成] 跨工具入口,Agent 每次会话必读
|
|
33
|
+
CLAUDE.md ← [生成] Claude Code 入口
|
|
34
|
+
.cursor/rules/ ← [生成] Cursor 规则
|
|
35
|
+
.agents/
|
|
36
|
+
manifest.yaml ← 你维护的唯一真相源
|
|
37
|
+
knowledge/ ← Agent 从代码推不出来的知识(领域、约定、决策)
|
|
38
|
+
contracts/ ← 对外接口的契约基线
|
|
39
|
+
playbooks/ ← 可复用的工作流(SKILL.md)
|
|
40
|
+
routing.md ← [生成] 按改动类型导航:改 UI 看哪、加接口看哪
|
|
41
|
+
modules.md ← [生成] 模块卡:每个子系统的职责/入口/坑
|
|
28
42
|
```
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
以后改工程知识 → 编辑 `manifest.yaml` → `harness-kit sync`。别手改生成物。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 命令
|
|
49
|
+
|
|
50
|
+
| 命令 | 作用 |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `harness-kit onboard` | 打印 onboard skill 给 Agent(配合 npx,永远最新、零安装) |
|
|
53
|
+
| `harness-kit init` | 铺 `.agents/` 骨架 + 空白 manifest |
|
|
54
|
+
| `harness-kit sync` | manifest → 生成 AGENTS.md / CLAUDE.md / Cursor rules / routing / modules |
|
|
55
|
+
| `harness-kit doctor` | 体检:完整性 / 路径引用 / 漂移 / 新鲜度 / 体量预算 |
|
|
56
|
+
| `harness-kit verify` | 门禁:跑不变量 + 契约 + 漂移,列出 GAPS,失败非 0 退出 |
|
|
57
|
+
| `harness-kit accept-contract` | 有意变更接口后,记录新的契约指纹为基线 |
|
|
58
|
+
| `harness-kit install-hooks` | 装 git 钩子:pre-commit 自动 sync + pre-push verify 拦漂移 |
|
|
59
|
+
|
|
60
|
+
所有命令都支持 `-C <dir>` 指定目标仓库(默认当前目录)。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 它解决什么问题
|
|
65
|
+
|
|
66
|
+
传统工程化(脚手架 / lint / CI)是**面向人、面向过去**的。Agent 需要的是显式、结构化、可机器消费的知识:
|
|
67
|
+
|
|
68
|
+
- **这是什么** → `identity`(name / summary / scope)
|
|
69
|
+
- **怎么跑** → `capabilities`(setup / build / test / dev…)
|
|
70
|
+
- **什么绝不能破** → `invariants`(声明式正则门禁)+ `contracts`(接口指纹基线)
|
|
71
|
+
- **改东西看哪** → `routing`(按改动类型导航)+ `modules`(模块卡)
|
|
72
|
+
- **哪些验证不了** → `GAPS`(诚实标注,绝不谎报)
|
|
73
|
+
|
|
74
|
+
harness-kit 把这些沉淀进 manifest,再确定性地生成与校验。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 工作原理
|
|
31
79
|
|
|
32
80
|
```
|
|
33
|
-
.agents/manifest.yaml (你 +
|
|
34
|
-
|
|
81
|
+
.agents/manifest.yaml (你 + Agent 维护的唯一源)
|
|
82
|
+
│
|
|
35
83
|
harness-kit sync (确定性生成,勿手改产物)
|
|
36
|
-
|
|
84
|
+
▼
|
|
37
85
|
AGENTS.md / CLAUDE.md / .cursor/rules / .agents/routing.md / .agents/modules.md
|
|
38
|
-
|
|
39
|
-
harness-kit verify (门禁:不变量
|
|
40
|
-
|
|
41
|
-
exit 0 / 非 0 + 诚实的 GAPS
|
|
86
|
+
│
|
|
87
|
+
harness-kit verify (门禁:不变量 + 契约 + 漂移)
|
|
88
|
+
▼
|
|
89
|
+
exit 0 / 非 0 + 诚实的 GAPS 清单
|
|
42
90
|
```
|
|
43
91
|
|
|
44
|
-
- **不变量**:声明式正则 `enforcement`(确定性、无 LLM),或标 `manual
|
|
45
|
-
-
|
|
46
|
-
- **新鲜度**:知识条目
|
|
47
|
-
- **GAPS**:打包 / 真网络 /
|
|
92
|
+
- **不变量**:声明式正则 `enforcement`(确定性、无 LLM),或标 `manual`
|
|
93
|
+
- **契约**:`snapshot` 打印接口指纹 → CLI 存基线并 diff(协议无关)
|
|
94
|
+
- **新鲜度**:知识条目 `binds` 源文件哈希,代码一动就告警
|
|
95
|
+
- **GAPS**:打包 / 真网络 / 生产上传等本地验证不了的,显式列出
|
|
48
96
|
|
|
49
|
-
|
|
97
|
+
---
|
|
50
98
|
|
|
51
|
-
|
|
99
|
+
## 让 Agent 自动触发(可选)
|
|
52
100
|
|
|
53
|
-
|
|
101
|
+
上面的"最快上手"是**显式调用**——你每次说"onboard 本仓库",Agent 才会做。如果你想让某个 Agent **自动识别**"该给仓库接 harness-kit",可以把 skill 软链进它的 skill 目录:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Cursor
|
|
105
|
+
ln -sf "$(npm root -g)/@erzhe/harness-kit/skills/erzhe-harness-init" ~/.cursor/skills/
|
|
106
|
+
|
|
107
|
+
# Claude Code
|
|
108
|
+
ln -sf "$(npm root -g)/@erzhe/harness-kit/skills/erzhe-harness-init" ~/.claude/skills/
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
这样当用户说"给这个仓库配置 harness"时,Agent 会自动找到并执行这个 skill。
|
|
54
112
|
|
|
55
|
-
|
|
56
|
-
- [`DESIGN.md`](DESIGN.md) —— 设计决策、调研与决策日志
|
|
113
|
+
---
|
|
57
114
|
|
|
58
115
|
## 开发
|
|
59
116
|
|
|
60
117
|
```bash
|
|
61
118
|
pnpm install
|
|
62
|
-
pnpm typecheck
|
|
63
|
-
pnpm test
|
|
64
|
-
pnpm build
|
|
65
|
-
pnpm exec tsx src/cli.ts verify --repo . #
|
|
119
|
+
pnpm typecheck # tsc --noEmit
|
|
120
|
+
pnpm test # node:test + tsx
|
|
121
|
+
pnpm build # esbuild → dist/harness-kit.cjs
|
|
122
|
+
pnpm exec tsx src/cli.ts verify --repo . # 自托管验证
|
|
66
123
|
```
|
|
67
124
|
|
|
125
|
+
## 文档
|
|
126
|
+
|
|
127
|
+
- [`SPEC-v0.md`](SPEC-v0.md) — manifest schema + 生成/校验契约
|
|
128
|
+
|
|
68
129
|
## License
|
|
69
130
|
|
|
70
131
|
MIT © [MeCKodo (二哲)](https://erzhe.me/)
|
package/SPEC-v0.md
CHANGED
|
@@ -165,7 +165,9 @@ generate:
|
|
|
165
165
|
5. 新鲜度:`knowledge.binds` 的源文件 hash 变了 → 报 drift;`invariants` 里 `manual:true` 的计入"待补门禁"技术债并给出数量。
|
|
166
166
|
6. **AGENTS.md 体量预算**:生成的 `AGENTS.md` 超过 ~150 行 / ~700 词告警。渐进式披露——入口只指路,细节放 `.agents/` 按需加载,绝不让 Agent 读不完。
|
|
167
167
|
|
|
168
|
-
`harness verify`(CI 门禁,全确定性)= 所有 `invariants` 的 `enforcement`/`check` + `contracts
|
|
168
|
+
`harness-kit verify`(CI 门禁,全确定性)= 所有 `invariants` 的 `enforcement`/`check` + `contracts` 的 `check`/`snapshot` + "生成物无漂移"。任一失败即 fail。
|
|
169
|
+
|
|
170
|
+
> **`verify` 不执行任何 `capabilities`**。`capabilities.verify` 只是给人 / agent 看的"如何验证本仓"指针;若真去执行会与自身无限递归。声明为 `mutating` / `background` 的 capability 一律进 GAPS(诚实标注"未在门禁里跑"),不实跑。所以放心声明 `setup: rush install`、`build: go build` 等——`verify` 不会在无网 / 无依赖环境里去跑它们。
|
|
169
171
|
|
|
170
172
|
**契约校验(v0.2,协议无关)**:契约的破坏检测有两条互补路径,CLI **不理解任何协议语义**,只当调度器 + 基线框架:
|
|
171
173
|
- `check`:仓库自带的破坏检测工具(HTTP → `oasdiff`;gRPC/protobuf → `buf breaking`;库 → `api-extractor`;CLI → 自写脚本),exit 0 = 兼容。CLI 只 `execSync` 看退出码。
|
package/dist/harness-kit.cjs
CHANGED
|
@@ -11214,11 +11214,11 @@ var require_out = __commonJS({
|
|
|
11214
11214
|
async.read(path2, getSettings(optionsOrSettingsOrCallback), callback);
|
|
11215
11215
|
}
|
|
11216
11216
|
exports2.stat = stat;
|
|
11217
|
-
function
|
|
11217
|
+
function statSync3(path2, optionsOrSettings) {
|
|
11218
11218
|
const settings = getSettings(optionsOrSettings);
|
|
11219
11219
|
return sync.read(path2, settings);
|
|
11220
11220
|
}
|
|
11221
|
-
exports2.statSync =
|
|
11221
|
+
exports2.statSync = statSync3;
|
|
11222
11222
|
function getSettings(settingsOrOptions = {}) {
|
|
11223
11223
|
if (settingsOrOptions instanceof settings_1.default) {
|
|
11224
11224
|
return settingsOrOptions;
|
|
@@ -11883,41 +11883,41 @@ var require_queue = __commonJS({
|
|
|
11883
11883
|
queue.drained = drained;
|
|
11884
11884
|
return queue;
|
|
11885
11885
|
function push(value) {
|
|
11886
|
-
var p = new Promise(function(
|
|
11886
|
+
var p = new Promise(function(resolve3, reject) {
|
|
11887
11887
|
pushCb(value, function(err2, result) {
|
|
11888
11888
|
if (err2) {
|
|
11889
11889
|
reject(err2);
|
|
11890
11890
|
return;
|
|
11891
11891
|
}
|
|
11892
|
-
|
|
11892
|
+
resolve3(result);
|
|
11893
11893
|
});
|
|
11894
11894
|
});
|
|
11895
11895
|
p.catch(noop);
|
|
11896
11896
|
return p;
|
|
11897
11897
|
}
|
|
11898
11898
|
function unshift(value) {
|
|
11899
|
-
var p = new Promise(function(
|
|
11899
|
+
var p = new Promise(function(resolve3, reject) {
|
|
11900
11900
|
unshiftCb(value, function(err2, result) {
|
|
11901
11901
|
if (err2) {
|
|
11902
11902
|
reject(err2);
|
|
11903
11903
|
return;
|
|
11904
11904
|
}
|
|
11905
|
-
|
|
11905
|
+
resolve3(result);
|
|
11906
11906
|
});
|
|
11907
11907
|
});
|
|
11908
11908
|
p.catch(noop);
|
|
11909
11909
|
return p;
|
|
11910
11910
|
}
|
|
11911
11911
|
function drained() {
|
|
11912
|
-
var p = new Promise(function(
|
|
11912
|
+
var p = new Promise(function(resolve3) {
|
|
11913
11913
|
process.nextTick(function() {
|
|
11914
11914
|
if (queue.idle()) {
|
|
11915
|
-
|
|
11915
|
+
resolve3();
|
|
11916
11916
|
} else {
|
|
11917
11917
|
var previousDrain = queue.drain;
|
|
11918
11918
|
queue.drain = function() {
|
|
11919
11919
|
if (typeof previousDrain === "function") previousDrain();
|
|
11920
|
-
|
|
11920
|
+
resolve3();
|
|
11921
11921
|
queue.drain = previousDrain;
|
|
11922
11922
|
};
|
|
11923
11923
|
}
|
|
@@ -12403,9 +12403,9 @@ var require_stream3 = __commonJS({
|
|
|
12403
12403
|
});
|
|
12404
12404
|
}
|
|
12405
12405
|
_getStat(filepath) {
|
|
12406
|
-
return new Promise((
|
|
12406
|
+
return new Promise((resolve3, reject) => {
|
|
12407
12407
|
this._stat(filepath, this._fsStatSettings, (error, stats) => {
|
|
12408
|
-
return error === null ?
|
|
12408
|
+
return error === null ? resolve3(stats) : reject(error);
|
|
12409
12409
|
});
|
|
12410
12410
|
});
|
|
12411
12411
|
}
|
|
@@ -12429,10 +12429,10 @@ var require_async5 = __commonJS({
|
|
|
12429
12429
|
this._readerStream = new stream_1.default(this._settings);
|
|
12430
12430
|
}
|
|
12431
12431
|
dynamic(root, options) {
|
|
12432
|
-
return new Promise((
|
|
12432
|
+
return new Promise((resolve3, reject) => {
|
|
12433
12433
|
this._walkAsync(root, options, (error, entries) => {
|
|
12434
12434
|
if (error === null) {
|
|
12435
|
-
|
|
12435
|
+
resolve3(entries);
|
|
12436
12436
|
} else {
|
|
12437
12437
|
reject(error);
|
|
12438
12438
|
}
|
|
@@ -12442,10 +12442,10 @@ var require_async5 = __commonJS({
|
|
|
12442
12442
|
async static(patterns, options) {
|
|
12443
12443
|
const entries = [];
|
|
12444
12444
|
const stream = this._readerStream.static(patterns, options);
|
|
12445
|
-
return new Promise((
|
|
12445
|
+
return new Promise((resolve3, reject) => {
|
|
12446
12446
|
stream.once("error", reject);
|
|
12447
12447
|
stream.on("data", (entry) => entries.push(entry));
|
|
12448
|
-
stream.once("end", () =>
|
|
12448
|
+
stream.once("end", () => resolve3(entries));
|
|
12449
12449
|
});
|
|
12450
12450
|
}
|
|
12451
12451
|
};
|
|
@@ -16471,7 +16471,7 @@ function useColor() {
|
|
|
16471
16471
|
var program = new Command();
|
|
16472
16472
|
|
|
16473
16473
|
// src/cli.ts
|
|
16474
|
-
var
|
|
16474
|
+
var import_node_path13 = require("node:path");
|
|
16475
16475
|
|
|
16476
16476
|
// src/commands/init.ts
|
|
16477
16477
|
var import_node_fs3 = require("node:fs");
|
|
@@ -16485,8 +16485,12 @@ function sha256(input) {
|
|
|
16485
16485
|
return (0, import_node_crypto.createHash)("sha256").update(input).digest("hex");
|
|
16486
16486
|
}
|
|
16487
16487
|
function hashFile(absPath) {
|
|
16488
|
-
|
|
16489
|
-
|
|
16488
|
+
try {
|
|
16489
|
+
if (!(0, import_node_fs2.statSync)(absPath).isFile()) return null;
|
|
16490
|
+
return sha256((0, import_node_fs2.readFileSync)(absPath));
|
|
16491
|
+
} catch {
|
|
16492
|
+
return null;
|
|
16493
|
+
}
|
|
16490
16494
|
}
|
|
16491
16495
|
function readText(absPath) {
|
|
16492
16496
|
return (0, import_node_fs2.existsSync)(absPath) ? (0, import_node_fs2.readFileSync)(absPath, "utf8") : null;
|
|
@@ -16841,6 +16845,7 @@ function syncCmd(repo) {
|
|
|
16841
16845
|
}
|
|
16842
16846
|
|
|
16843
16847
|
// src/commands/doctor.ts
|
|
16848
|
+
var import_fast_glob = __toESM(require_out4(), 1);
|
|
16844
16849
|
var import_node_fs5 = require("node:fs");
|
|
16845
16850
|
var import_node_path7 = require("node:path");
|
|
16846
16851
|
var AGENTS_MAX_LINES = 150;
|
|
@@ -16859,6 +16864,18 @@ function doctorCmd(repo) {
|
|
|
16859
16864
|
warn(i.msg);
|
|
16860
16865
|
}
|
|
16861
16866
|
}
|
|
16867
|
+
for (const inv of m.invariants ?? []) {
|
|
16868
|
+
if (!inv.enforcement) continue;
|
|
16869
|
+
const globs = inv.enforcement.path_glob?.length ? inv.enforcement.path_glob : ["**/*"];
|
|
16870
|
+
const n = import_fast_glob.default.sync(globs, {
|
|
16871
|
+
cwd: repo,
|
|
16872
|
+
onlyFiles: true,
|
|
16873
|
+
dot: false,
|
|
16874
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
|
|
16875
|
+
}).length;
|
|
16876
|
+
if (n === 0)
|
|
16877
|
+
warn(`invariant ${inv.id}: enforcement path_glob matches 0 files \u2014 passes without checking anything (wrong path_glob for this repo layout?)`);
|
|
16878
|
+
}
|
|
16862
16879
|
info("\n2) Referenced paths");
|
|
16863
16880
|
const checkPath = (rel, label) => {
|
|
16864
16881
|
if ((0, import_node_fs5.existsSync)((0, import_node_path7.join)(repo, ".agents", rel))) ok(`${label}: ${rel}`);
|
|
@@ -16867,19 +16884,24 @@ function doctorCmd(repo) {
|
|
|
16867
16884
|
problems++;
|
|
16868
16885
|
}
|
|
16869
16886
|
};
|
|
16870
|
-
const checkRepoPath = (rel, label) => {
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16887
|
+
const checkRepoPath = (rel, label, wantFile = false) => {
|
|
16888
|
+
const abs = (0, import_node_path7.join)(repo, rel);
|
|
16889
|
+
if (!(0, import_node_fs5.existsSync)(abs)) {
|
|
16890
|
+
err(`${label} points at a missing path: ${rel}`);
|
|
16874
16891
|
problems++;
|
|
16875
|
-
}
|
|
16892
|
+
} else if (wantFile && (0, import_node_fs5.statSync)(abs).isDirectory()) {
|
|
16893
|
+
warn(`${label}: ${rel} is a directory \u2014 entry/binds should be a file (freshness hashes file content)`);
|
|
16894
|
+
} else ok(`${label}: ${rel}`);
|
|
16876
16895
|
};
|
|
16877
|
-
for (const k of m.knowledge ?? [])
|
|
16896
|
+
for (const k of m.knowledge ?? []) {
|
|
16897
|
+
checkPath(k.path, "knowledge");
|
|
16898
|
+
for (const b of new Set(k.binds ?? [])) checkRepoPath(b, `knowledge "${k.path}" binds`, true);
|
|
16899
|
+
}
|
|
16878
16900
|
if (m.playbooks?.dir) checkPath(m.playbooks.dir, "playbooks");
|
|
16879
16901
|
for (const r of m.routing ?? [])
|
|
16880
16902
|
for (const p of /* @__PURE__ */ new Set([...r.read ?? [], ...r.entry ?? []])) checkRepoPath(p, `routing "${r.when}"`);
|
|
16881
16903
|
for (const mod of m.modules ?? [])
|
|
16882
|
-
for (const p of new Set(mod.entry ?? [])) checkRepoPath(p, `module ${mod.name}
|
|
16904
|
+
for (const p of new Set(mod.entry ?? [])) checkRepoPath(p, `module ${mod.name}`, true);
|
|
16883
16905
|
info("\n3) Generated files drift");
|
|
16884
16906
|
for (const [rel, content] of renderTargets(m)) {
|
|
16885
16907
|
const cur = readText((0, import_node_path7.join)(repo, rel));
|
|
@@ -16961,13 +16983,13 @@ function writeBaseline(repo, id, content) {
|
|
|
16961
16983
|
}
|
|
16962
16984
|
|
|
16963
16985
|
// src/enforce.ts
|
|
16964
|
-
var
|
|
16986
|
+
var import_fast_glob2 = __toESM(require_out4(), 1);
|
|
16965
16987
|
var import_node_fs6 = require("node:fs");
|
|
16966
16988
|
var import_node_path9 = require("node:path");
|
|
16967
16989
|
var DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.agents/map/**"];
|
|
16968
16990
|
function runEnforcement(repo, invId, e) {
|
|
16969
16991
|
const globs = e.path_glob?.length ? e.path_glob : ["**/*"];
|
|
16970
|
-
const files =
|
|
16992
|
+
const files = import_fast_glob2.default.sync(globs, {
|
|
16971
16993
|
cwd: repo,
|
|
16972
16994
|
onlyFiles: true,
|
|
16973
16995
|
dot: false,
|
|
@@ -17142,6 +17164,117 @@ function acceptContractCmd(repo, id) {
|
|
|
17142
17164
|
return problems ? 1 : 0;
|
|
17143
17165
|
}
|
|
17144
17166
|
|
|
17167
|
+
// src/commands/install-hooks.ts
|
|
17168
|
+
var import_node_child_process4 = require("node:child_process");
|
|
17169
|
+
var import_node_fs7 = require("node:fs");
|
|
17170
|
+
var import_node_path11 = require("node:path");
|
|
17171
|
+
var MARK = "harness-kit-managed-hook";
|
|
17172
|
+
function hooksDir(repo) {
|
|
17173
|
+
const out = (0, import_node_child_process4.execSync)("git rev-parse --git-path hooks", { cwd: repo, encoding: "utf8" }).trim();
|
|
17174
|
+
return (0, import_node_path11.resolve)(repo, out);
|
|
17175
|
+
}
|
|
17176
|
+
function preCommit() {
|
|
17177
|
+
return `#!/bin/sh
|
|
17178
|
+
# ${MARK} (pre-commit): regenerate agent files from the manifest and stage them,
|
|
17179
|
+
# so generated docs never drift from .agents/manifest.yaml.
|
|
17180
|
+
# Reset with \`harness-kit install-hooks --force\`; delete this file to remove.
|
|
17181
|
+
[ -f .agents/manifest.yaml ] || exit 0
|
|
17182
|
+
HK="\${HARNESS_KIT_CMD:-npx -y @erzhe/harness-kit@latest}"
|
|
17183
|
+
if ! $HK sync --repo . >/dev/null; then
|
|
17184
|
+
echo "harness-kit: sync failed (run \\\`$HK sync\\\` to see why)" >&2
|
|
17185
|
+
exit 1
|
|
17186
|
+
fi
|
|
17187
|
+
git add AGENTS.md CLAUDE.md .cursor/rules .agents >/dev/null 2>&1 || true
|
|
17188
|
+
exit 0
|
|
17189
|
+
`;
|
|
17190
|
+
}
|
|
17191
|
+
function prePush() {
|
|
17192
|
+
return `#!/bin/sh
|
|
17193
|
+
# ${MARK} (pre-push): block the push when the agent context has drifted from code.
|
|
17194
|
+
# Bypass once with \`git push --no-verify\`.
|
|
17195
|
+
[ -f .agents/manifest.yaml ] || exit 0
|
|
17196
|
+
HK="\${HARNESS_KIT_CMD:-npx -y @erzhe/harness-kit@latest}"
|
|
17197
|
+
if ! $HK verify --repo .; then
|
|
17198
|
+
echo "" >&2
|
|
17199
|
+
echo "harness-kit: verify failed \u2014 agent context drifted from code." >&2
|
|
17200
|
+
echo " self-heal: run an agent with \\\`$HK onboard\\\` and follow its Maintenance section," >&2
|
|
17201
|
+
echo " then review the diff. Bypass once with \\\`git push --no-verify\\\`." >&2
|
|
17202
|
+
exit 1
|
|
17203
|
+
fi
|
|
17204
|
+
exit 0
|
|
17205
|
+
`;
|
|
17206
|
+
}
|
|
17207
|
+
function installHooksCmd(repo, force = false) {
|
|
17208
|
+
let dir;
|
|
17209
|
+
try {
|
|
17210
|
+
dir = hooksDir(repo);
|
|
17211
|
+
} catch {
|
|
17212
|
+
err("not a git repo (git rev-parse failed) \u2014 run inside a git working tree");
|
|
17213
|
+
return 1;
|
|
17214
|
+
}
|
|
17215
|
+
const targets = [
|
|
17216
|
+
["pre-commit", preCommit()],
|
|
17217
|
+
["pre-push", prePush()]
|
|
17218
|
+
];
|
|
17219
|
+
for (const [name, body] of targets) {
|
|
17220
|
+
const p = (0, import_node_path11.join)(dir, name);
|
|
17221
|
+
if ((0, import_node_fs7.existsSync)(p) && !force) {
|
|
17222
|
+
const cur = (0, import_node_fs7.readFileSync)(p, "utf8");
|
|
17223
|
+
if (!cur.includes(MARK)) {
|
|
17224
|
+
warn(`${name} exists and isn't harness-kit-managed \u2014 skipped (use --force to overwrite)`);
|
|
17225
|
+
continue;
|
|
17226
|
+
}
|
|
17227
|
+
}
|
|
17228
|
+
writeText(p, body);
|
|
17229
|
+
(0, import_node_fs7.chmodSync)(p, 493);
|
|
17230
|
+
ok(`installed ${name}`);
|
|
17231
|
+
}
|
|
17232
|
+
info(`
|
|
17233
|
+
Hooks written to ${dir}`);
|
|
17234
|
+
info("pre-commit: regenerates agent files from the manifest and stages them.");
|
|
17235
|
+
info("pre-push: runs `harness-kit verify` and blocks on drift (bypass: git push --no-verify).");
|
|
17236
|
+
info("override the CLI it calls with env HARNESS_KIT_CMD (e.g. for local dev).");
|
|
17237
|
+
return 0;
|
|
17238
|
+
}
|
|
17239
|
+
|
|
17240
|
+
// src/commands/onboard.ts
|
|
17241
|
+
var import_node_fs8 = require("node:fs");
|
|
17242
|
+
var import_node_path12 = require("node:path");
|
|
17243
|
+
var import_node_url = require("node:url");
|
|
17244
|
+
var import_meta = {};
|
|
17245
|
+
function moduleDir() {
|
|
17246
|
+
if (typeof __dirname !== "undefined" && __dirname) return __dirname;
|
|
17247
|
+
return (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
17248
|
+
}
|
|
17249
|
+
var HERE = moduleDir();
|
|
17250
|
+
var SKILL_REL = "skills/erzhe-harness-init/SKILL.md";
|
|
17251
|
+
var META = `<!-- harness-kit onboard -->
|
|
17252
|
+
You are an AI agent onboarding the CURRENT repository to harness-kit.
|
|
17253
|
+
Follow the skill below verbatim. Run every harness-kit command through
|
|
17254
|
+
\`npx -y @erzhe/harness-kit@latest <cmd>\` \u2014 always the latest version, no global install.
|
|
17255
|
+
Do not fabricate: confirm uncertain fields with the user, and report honest GAPS.
|
|
17256
|
+
|
|
17257
|
+
---
|
|
17258
|
+
|
|
17259
|
+
`;
|
|
17260
|
+
function resolveSkill() {
|
|
17261
|
+
for (const up of ["..", (0, import_node_path12.join)("..", "..")]) {
|
|
17262
|
+
const p = (0, import_node_path12.join)(HERE, up, SKILL_REL);
|
|
17263
|
+
if ((0, import_node_fs8.existsSync)(p)) return p;
|
|
17264
|
+
}
|
|
17265
|
+
return null;
|
|
17266
|
+
}
|
|
17267
|
+
function onboardCmd() {
|
|
17268
|
+
const p = resolveSkill();
|
|
17269
|
+
if (!p) {
|
|
17270
|
+
err(`bundled skill not found (${SKILL_REL})`);
|
|
17271
|
+
return 1;
|
|
17272
|
+
}
|
|
17273
|
+
process.stdout.write(META);
|
|
17274
|
+
process.stdout.write((0, import_node_fs8.readFileSync)(p, "utf8"));
|
|
17275
|
+
return 0;
|
|
17276
|
+
}
|
|
17277
|
+
|
|
17145
17278
|
// src/cli.ts
|
|
17146
17279
|
function guard(fn) {
|
|
17147
17280
|
try {
|
|
@@ -17153,13 +17286,15 @@ function guard(fn) {
|
|
|
17153
17286
|
}
|
|
17154
17287
|
}
|
|
17155
17288
|
var program2 = new Command();
|
|
17156
|
-
program2.name("harness-kit").description("AI-friendly repo harness").version("0.1.
|
|
17157
|
-
var repoOf = (o) => (0,
|
|
17289
|
+
program2.name("harness-kit").description("AI-friendly repo harness").version("0.1.2");
|
|
17290
|
+
var repoOf = (o) => (0, import_node_path13.resolve)(o.repo);
|
|
17158
17291
|
program2.command("init").description("scaffold .agents/ skeleton + starter manifest").option("-C, --repo <dir>", "target repo dir", process.cwd()).option("--name <name>", "project name", "my-project").option("--force", "overwrite existing manifest", false).action((o) => guard(() => initCmd(repoOf(o), o.name, o.force)));
|
|
17159
17292
|
program2.command("sync").description("generate tool files (AGENTS.md, CLAUDE.md, .cursor rules) from manifest").option("-C, --repo <dir>", "target repo dir", process.cwd()).action((o) => guard(() => syncCmd(repoOf(o))));
|
|
17160
17293
|
program2.command("doctor").description("dev-time health check: completeness, drift, freshness, tech debt").option("-C, --repo <dir>", "target repo dir", process.cwd()).action((o) => guard(() => doctorCmd(repoOf(o))));
|
|
17161
17294
|
program2.command("verify").description("CI gate: run enforceable invariants + contracts + drift; nonzero on failure").option("-C, --repo <dir>", "target repo dir", process.cwd()).action((o) => guard(() => verifyCmd(repoOf(o))));
|
|
17162
17295
|
program2.command("accept-contract").description("record current contract fingerprint(s) as the accepted baseline (after an intended change)").option("-C, --repo <dir>", "target repo dir", process.cwd()).option("--id <id>", "only this contract (default: all with a snapshot command)").action((o) => guard(() => acceptContractCmd(repoOf(o), o.id)));
|
|
17296
|
+
program2.command("install-hooks").description("install git hooks: pre-commit auto-sync + pre-push verify (drift gate)").option("-C, --repo <dir>", "target repo dir", process.cwd()).option("--force", "overwrite existing hooks", false).action((o) => guard(() => installHooksCmd(repoOf(o), o.force)));
|
|
17297
|
+
program2.command("onboard").description("print the erzhe-harness-init skill for an agent to follow (use via npx, always latest)").action(() => guard(() => onboardCmd()));
|
|
17163
17298
|
program2.parseAsync();
|
|
17164
17299
|
/*! Bundled license information:
|
|
17165
17300
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erzhe/harness-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI-friendly repo harness: a manifest.yaml single source of truth that generates AGENTS.md / CLAUDE.md / Cursor rules and enforces invariants, contracts and drift via `harness-kit verify`.",
|
|
6
6
|
"keywords": [
|
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"harness": "tsx src/cli.ts",
|
|
44
|
-
"build": "esbuild src/cli.ts --bundle --platform=node --format=cjs --target=node18 --outfile=dist/harness-kit.cjs && chmod +x dist/harness-kit.cjs",
|
|
44
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --format=cjs --target=node18 --log-override:empty-import-meta=silent --outfile=dist/harness-kit.cjs && chmod +x dist/harness-kit.cjs",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"test": "node --import tsx --test test/*.test.ts",
|
|
47
|
+
"test:repos": "bash scripts/regression.sh",
|
|
47
48
|
"prepare": "pnpm build"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
@@ -13,11 +13,11 @@ CLI 只负责机械骨架 + 确定性门禁;**理解仓库、把隐性知识
|
|
|
13
13
|
|
|
14
14
|
按可用性择一,后续统一用 `harness-kit` 指代(包名 `@erzhe/harness-kit`,bin 是 `harness-kit`):
|
|
15
15
|
|
|
16
|
+
- **推荐(零安装、永远最新)**:`npx -y @erzhe/harness-kit@latest`
|
|
16
17
|
- 已全局安装:`harness-kit`
|
|
17
|
-
- 未安装、临时用:`npx @erzhe/harness-kit`
|
|
18
18
|
- 在 harness-kit 仓内开发:`pnpm exec tsx src/cli.ts`
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
先跑一次 `--help` 确认可用(如 `npx -y @erzhe/harness-kit@latest --help`)。全程用同一种方式,别混用版本。
|
|
21
21
|
|
|
22
22
|
## 1. 铺骨架(若还没有 .agents/)
|
|
23
23
|
|
|
@@ -40,9 +40,9 @@ harness-kit init --repo .
|
|
|
40
40
|
1. **identity**:读 `README` / `package.json` / 顶层目录。填 `name`、一句话 `summary`、`scope_in` / `scope_out`(哪些该改、哪些别碰)、`upstream` / `downstream`。
|
|
41
41
|
2. **capabilities**:从 `package.json` scripts、`Makefile`、`justfile`、CI 配置推断常用命令(setup / build / test / dev / lint / release)。给 `run`;长驻的标 `background: true`,有副作用的标 `mutating: true`。
|
|
42
42
|
3. **environment**:从 `.env.example` / README 抓关键环境变量。危险的标 `secret: true`、必需的标 `required: true`。
|
|
43
|
-
4. **contracts**:识别对外接口——HTTP 路由文件、CLI flags、导出的公共 API、proto/schema、事件。**尽量给 `snapshot` 命令**(打印当前接口指纹到 stdout,如 `grep -oE '"/api/[^"]*"' src/routes.ts | sort -u`),CLI 会存基线并 diff。给不出自动检查的填 `manual_verify`
|
|
43
|
+
4. **contracts**:识别对外接口——HTTP 路由文件、CLI flags、导出的公共 API、proto/schema/IDL、事件。**尽量给 `snapshot` 命令**(打印当前接口指纹到 stdout,如 `grep -oE '"/api/[^"]*"' src/routes.ts | sort -u`),CLI 会存基线并 diff。给不出自动检查的填 `manual_verify` 说明(进 GAPS)。两条纪律:(a) **过滤注释/生成物噪音**——配置里被注释掉的示例条目会污染指纹、日后清注释误报 drift,必要时先 `sed 's://.*::'` 剥注释再抓;(b) **命令含引号/正则/管道就下沉到 `.agents/checks/<id>.sh`**(脚本里 `cd` 到仓库根 + `LC_ALL=C` 保跨机字节一致),manifest 里写 `snapshot: bash .agents/checks/<id>.sh`——比把转义地狱硬塞进 YAML 干净,过滤规则也可 review。
|
|
44
44
|
5. **invariants**:从 `CONTRIBUTING` / 现有 `AGENTS.md` / 代码约定提炼"必须始终成立"的规则。**能写成正则的用 `enforcement`**(`forbid_pattern` / `require_pattern` + `path_glob` 限定作用域,glob 是 include-only);否则标 `manual: true`。
|
|
45
|
-
6. **modules**:主要子系统各写一张模块卡:`role`、`entry
|
|
45
|
+
6. **modules**:主要子系统各写一张模块卡:`role`、`entry`、`upstream` / `downstream`、`must_know`、`pitfalls`(常见坑——最高价值的一栏)。⚠️ `entry`(以及 knowledge 的 `binds`)必须指到**具体文件**(如 `src/index.ts`),**不能是目录**——它要 hash 文件内容做新鲜度绑定;`routing.read` 用目录则 OK。
|
|
46
46
|
7. **routing**:把常见改动类型(修 bug / 加接口 / 改 UI / 动权限 / 发版…)各写一行:`read`(先读哪些文件)、`entry`、`dont_assume`(别瞎猜什么)、`verify`(最少验证步骤,引用 capability 动词或原始命令)。
|
|
47
47
|
8. **knowledge**:把 agent 从代码推断不出来的东西写进 `.agents/knowledge/`;重要决策建一篇 journal ADR(`knowledge/journal/NNNN-*.md`)。用 `binds` 把知识条目绑定到源文件,源文件变了会触发新鲜度告警。**只沉淀非显然的知识,别记一次性噪音或代码里显而易见的东西。**
|
|
48
48
|
|
|
@@ -66,7 +66,36 @@ harness-kit verify # 门禁:跑不变量 + 契约 + 漂移,并列
|
|
|
66
66
|
|
|
67
67
|
- `harness-kit verify` 退出码 0;`GAPS` 段里每一条都是**真的没法自动查**(打包 / 真网络 / 生产上传 / 人肉不变量),不是偷懒。
|
|
68
68
|
- `AGENTS.md` 在体量预算内(`doctor` 第 6 项会报行数/词数)。
|
|
69
|
-
- 把生成物 + `.agents/` + 契约基线一起提交;告诉用户:**以后改 manifest → `harness-kit sync
|
|
69
|
+
- 把生成物 + `.agents/` + 契约基线一起提交;告诉用户:**以后改 manifest → `harness-kit sync`,别手改生成物**。
|
|
70
|
+
- 装上自动化门禁,让防腐烂不靠人自律:
|
|
71
|
+
```
|
|
72
|
+
harness-kit install-hooks --repo .
|
|
73
|
+
```
|
|
74
|
+
pre-commit 自动 sync 并暂存生成物、pre-push 跑 `verify` 拦漂移;团队再把 `harness-kit verify` 放进 CI,才能真正兑现"跨人跨机一致"。
|
|
75
|
+
|
|
76
|
+
## 5. 日常维护 / 自愈(接入之后,长期靠这个防腐烂)
|
|
77
|
+
|
|
78
|
+
harness-kit 的持续价值不在首次接入,而在**代码演进时上下文不腐烂**。机制是:门禁发现漂移 → agent 自动修 → 人只 review。**别把维护活丢回给人**——你(agent)来修,人只做最后一次审查。
|
|
79
|
+
|
|
80
|
+
**触发**:`harness-kit verify` 非零,或 `doctor` 报 drift / 新鲜度告警(装了 pre-push hook 会在 push 时自动拦)。
|
|
81
|
+
|
|
82
|
+
**自愈流程(你就是那个 agent,按此做):**
|
|
83
|
+
1. 跑 `harness-kit verify`,逐条读红灯——它已把"哪块过期 / 哪个契约变了 / 该 sync 还是该 accept"写清楚了。
|
|
84
|
+
2. 逐条处理:
|
|
85
|
+
- **生成物 drift**("AGENTS.md drifted, run sync")→ 直接 `harness-kit sync`。
|
|
86
|
+
- **knowledge 新鲜度告警**(绑定的源文件变了)→ 重新读那个源文件,判断知识是否真过时;过时就改 `.agents/knowledge/*` 或对应 manifest 字段再 sync。**没实质变化就别乱改**。
|
|
87
|
+
- **契约 drift**(snapshot 变了)→ 看对应代码改动:确属**有意**改接口(有需求 / commit 佐证)才 `harness-kit accept-contract --id <id>`;拿不准是否 breaking 就**别 accept**,标给人。
|
|
88
|
+
- **invariant 违规** → 按规则修代码;确认该规则已过时才动 manifest。
|
|
89
|
+
3. 全部处理完再 `verify` 到退出码 0。
|
|
90
|
+
4. **产出一份变更清单交给人 review(这是唯一的人工动作)**,至少含:
|
|
91
|
+
- 改了哪些 manifest 字段 / knowledge 文件,为什么;
|
|
92
|
+
- accept 了哪些契约、对应什么代码改动、是否 breaking;
|
|
93
|
+
- 哪些**没动**及原因。
|
|
94
|
+
|
|
95
|
+
**红线**:
|
|
96
|
+
- 契约 accept 必须有据可依且**必写进给人的清单**——绝不静默 auto-accept 对外接口变更。
|
|
97
|
+
- 拿不准就标 GAP 交人,别用幻觉填。
|
|
98
|
+
- 只修真漂移,别为了凑绿去改无关内容。
|
|
70
99
|
|
|
71
100
|
## 反模式(别做)
|
|
72
101
|
|