@erzhe/harness-kit 0.1.1 → 0.1.3

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 CHANGED
@@ -1,81 +1,147 @@
1
1
  # @erzhe/harness-kit
2
2
 
3
- > AI-friendly repo harness —— 把工程知识变成 agent 可消费的**契约层**。
3
+ > Agent 发一句话,让它帮你把仓库变成 AI 友好的。
4
4
 
5
- 一份 `.agents/manifest.yaml` 作为单一事实源,生成各家 agent 的入口文件(`AGENTS.md` / `CLAUDE.md` / Cursor rules),并用**门禁**保证不变量、对外契约和文档不漂移。让不同的人、不同的机器上的 agent,构建出**一模一样的范式**。
5
+ harness-kit 把仓库的工程知识(这是什么、怎么跑、什么不能破、改东西看哪)沉淀成一份 `.agents/manifest.yaml`,然后自动生成各家 Agent 的入口文件(`AGENTS.md` / `CLAUDE.md`),并用门禁保证它们始终一致。
6
6
 
7
- > 包名 `@erzhe/harness-kit`,安装后命令行工具叫 **`harness-kit`**。
7
+ ---
8
8
 
9
- ## 最快上手(推荐,零安装)
9
+ ## 最快上手
10
10
 
11
- 在**任意仓库**里,把这句丢给你的 agent(Cursor / Claude Code / Codex 通用):
11
+ 在**任意仓库**里,把这句话丢给你的 Agent(Cursor / Claude Code / Codex 通用):
12
12
 
13
- > Onboard 本仓库到 harness-kit:跑 `npx -y @erzhe/harness-kit@latest onboard`,然后严格按输出执行。
13
+ ```
14
+ Onboard 本仓库到 harness-kit:跑 npx -y @erzhe/harness-kit@latest onboard,然后严格按输出执行。
15
+ ```
14
16
 
15
- agent 会拉取**最新版**的 onboard skill 并照着做:仓库考古 → 逐块跟你确认着填 `.agents/manifest.yaml` → `sync` / `doctor` / `verify` 到绿。全程走 `npx`,不往机器上装任何全局东西;你一发新版,所有人下次执行就用上了。
17
+ 就这一句。Agent 会:
16
18
 
17
- ## 为什么
19
+ 1. 拉取最新版的 onboard skill
20
+ 2. 扫描你的仓库(读 README、package.json、目录结构…)
21
+ 3. 逐块跟你确认着填 `.agents/manifest.yaml`
22
+ 4. 跑 `sync` / `doctor` / `verify` 直到全绿
23
+ 5. 装上 git 钩子(`install-hooks`):之后 pre-commit 自动 sync、pre-push verify 拦漂移,维护不用你记命令
18
24
 
19
- 传统工程化(脚手架 / lint / CI)是**面向过去、面向人**的。Agent 需要的是显式、结构化、可机器消费的知识:这个仓库是什么、能跑什么、什么绝不能破、改动该看哪、哪些验证不了。`harness-kit` 把这些沉淀进 manifest,再确定性地生成与校验。
25
+ 全程走 `npx`,不往机器上装任何全局东西;你一发新版,所有人下次执行就用上了。
20
26
 
21
- ## 安装
27
+ ---
28
+
29
+ ## 初始化后你得到什么
22
30
 
23
- ```bash
24
- npm i -g @erzhe/harness-kit # 或 pnpm add -g / 直接 npx @erzhe/harness-kit
25
- harness-kit --help
31
+ ```
32
+ 你的仓库/
33
+ AGENTS.md ← [生成] 跨工具入口,Agent 每次会话必读
34
+ CLAUDE.md ← [生成] Claude Code 入口
35
+ .agents/
36
+ manifest.yaml ← 你维护的唯一真相源
37
+ knowledge/ ← Agent 从代码推不出来的知识(领域、约定、决策)
38
+ contracts/ ← 对外接口的契约基线
39
+ playbooks/ ← 可复用的工作流(SKILL.md)
40
+ routing.md ← [生成] 按改动类型导航:改 UI 看哪、加接口看哪
41
+ modules.md ← [生成] 模块卡:每个子系统的职责/入口/坑
26
42
  ```
27
43
 
28
- ## 命令
44
+ 以后改工程知识 → 编辑 `manifest.yaml` → `harness-kit sync`。别手改生成物。
45
+
46
+ ---
47
+
48
+ ## 接入之后:自动防腐烂,人只 review
49
+
50
+ harness-kit 的长期价值不在首次接入,而在**代码演进时上下文不腐烂**——而且这件事不该靠人记命令:
51
+
52
+ - **装了钩子就无感**:`install-hooks` 之后,pre-commit 自动重生成入口文件、pre-push 跑 `verify`,上下文和代码对不上就红灯拦下。
53
+ - **漂移了让 Agent 自愈**:知识过期 / 接口契约变了,把仓库丢给 Agent 说一句 `onboard`,它会照 skill 自动考古、改 manifest/knowledge、有据才 accept 契约,最后**产出一份变更清单**。
54
+ - **你只做一个动作**:审查那份 diff,批准或打回。生产和更新都交给 Agent,人只 review。
29
55
 
30
56
  ```
31
- harness-kit onboard # 打印 onboard skill 给 agent(配合 npx,永远最新、零安装)
32
- harness-kit init # .agents/ 骨架 + starter manifest
33
- harness-kit sync # manifest -> AGENTS.md / CLAUDE.md / Cursor rules / routing / modules
34
- harness-kit doctor # 开发态体检:补全度 / 引用路径 / 漂移 / 新鲜度 / 体量预算
35
- harness-kit verify # 门禁(CI):跑不变量 + 契约 + 漂移,列出 GAPS,失败非 0 退出
36
- harness-kit accept-contract # 一次预期的接口变更后,记录新的契约指纹为基线
57
+ 代码演进 [hook/CI] verify 红灯,指出哪块过期/哪个契约变了
58
+ [Agent] 自动修 + 产出变更清单
59
+ [你] review:批准 or 打回 ← 唯一的人工动作
37
60
  ```
38
61
 
39
- ## 心智模型
62
+ ---
63
+
64
+ ## 命令
65
+
66
+ | 命令 | 作用 |
67
+ | --- | --- |
68
+ | `harness-kit onboard` | 打印 onboard skill 给 Agent(配合 npx,永远最新、零安装) |
69
+ | `harness-kit init` | 铺 `.agents/` 骨架 + 空白 manifest |
70
+ | `harness-kit sync` | manifest → 生成 AGENTS.md / CLAUDE.md / routing / modules |
71
+ | `harness-kit doctor` | 体检:完整性 / 路径引用 / 漂移 / 新鲜度 / 体量预算 |
72
+ | `harness-kit verify` | 门禁:跑不变量 + 契约 + 漂移,列出 GAPS,失败非 0 退出 |
73
+ | `harness-kit accept-contract` | 有意变更接口后,记录新的契约指纹为基线 |
74
+ | `harness-kit install-hooks` | 装 git 钩子:pre-commit 自动 sync + pre-push verify 拦漂移 |
75
+
76
+ 所有命令都支持 `-C <dir>` 指定目标仓库(默认当前目录)。
77
+
78
+ ---
79
+
80
+ ## 它解决什么问题
81
+
82
+ 传统工程化(脚手架 / lint / CI)是**面向人、面向过去**的。Agent 需要的是显式、结构化、可机器消费的知识:
83
+
84
+ - **这是什么** → `identity`(name / summary / scope)
85
+ - **怎么跑** → `capabilities`(setup / build / test / dev…)
86
+ - **什么绝不能破** → `invariants`(声明式正则门禁)+ `contracts`(接口指纹基线)
87
+ - **改东西看哪** → `routing`(按改动类型导航)+ `modules`(模块卡)
88
+ - **哪些验证不了** → `GAPS`(诚实标注,绝不谎报)
89
+
90
+ harness-kit 把这些沉淀进 manifest,再确定性地生成与校验。
91
+
92
+ ---
93
+
94
+ ## 工作原理
40
95
 
41
96
  ```
42
- .agents/manifest.yaml (你 + agent 维护的唯一源)
43
- |
97
+ .agents/manifest.yaml (你 + Agent 维护的唯一源)
98
+
44
99
  harness-kit sync (确定性生成,勿手改产物)
45
- v
46
- AGENTS.md / CLAUDE.md / .cursor/rules / .agents/routing.md / .agents/modules.md
47
- |
48
- harness-kit verify (门禁:不变量 enforcement + 契约 snapshot + 生成物/知识漂移)
49
- v
50
- exit 0 / 非 0 + 诚实的 GAPS 清单(查不了的东西绝不谎报)
100
+
101
+ AGENTS.md / CLAUDE.md / .agents/routing.md / .agents/modules.md
102
+
103
+ harness-kit verify (门禁:不变量 + 契约 + 漂移)
104
+
105
+ exit 0 / 非 0 + 诚实的 GAPS 清单
51
106
  ```
52
107
 
53
- - **不变量**:声明式正则 `enforcement`(确定性、无 LLM),或标 `manual`。
54
- - **契约**:协议无关——`check`(跑仓库自带工具看退出码)或 `snapshot`(打印接口指纹 → CLI 存基线并 diff)。
55
- - **新鲜度**:知识条目 / 模块卡 `binds` 源文件哈希,代码一动就告警,无需人肉维护日期。
56
- - **GAPS**:打包 / 真网络 / 生产上传等本地验证不了的,`verify` 显式列出,绝不假装通过。
108
+ - **不变量**:声明式正则 `enforcement`(确定性、无 LLM),或标 `manual`
109
+ - **契约**:`snapshot` 打印接口指纹 → CLI 存基线并 diff(协议无关)
110
+ - **新鲜度**:知识条目 `binds` 源文件哈希,代码一动就告警
111
+ - **GAPS**:打包 / 真网络 / 生产上传等本地验证不了的,显式列出
57
112
 
58
- ## 让 agent 来配置:onboard skill
113
+ ---
59
114
 
60
- 手写 manifest 门槛高,但对 agent 是绝配。随包附带的 [`skills/erzhe-harness-init`](skills/erzhe-harness-init/SKILL.md) 引导 agent 做仓库考古、逐块跟你确认着填 manifest、再跑 `sync/doctor/verify` 到绿。
115
+ ## Agent 自动触发(可选)
61
116
 
62
- 最省事的用法就是上面的 [最快上手](#最快上手推荐零安装)——`npx ... onboard` skill 打印给 agent,零安装、永远最新。若想让某个 agent **自动触发**(不用每次点名),可选地把 skill 软链进它的 skill 目录:`ln -sf "$(npm root -g)/@erzhe/harness-kit/skills/erzhe-harness-init" ~/.cursor/skills/`。
117
+ 上面的"最快上手"是**显式调用**——你每次说"onboard 本仓库",Agent 才会做。如果你想让某个 Agent **自动识别**"该给仓库接 harness-kit",可以把 skill 软链进它的 skill 目录:
63
118
 
64
- ## 文档
119
+ ```bash
120
+ # Cursor
121
+ ln -sf "$(npm root -g)/@erzhe/harness-kit/skills/erzhe-harness-init" ~/.cursor/skills/
122
+
123
+ # Claude Code
124
+ ln -sf "$(npm root -g)/@erzhe/harness-kit/skills/erzhe-harness-init" ~/.claude/skills/
125
+ ```
126
+
127
+ 这样当用户说"给这个仓库配置 harness"时,Agent 会自动找到并执行这个 skill。
65
128
 
66
- - [`SPEC-v0.md`](SPEC-v0.md) —— manifest schema + 生成/校验契约(当前 v0.2)
67
- - [`DESIGN.md`](DESIGN.md) —— 设计决策、调研与决策日志
129
+ ---
68
130
 
69
131
  ## 开发
70
132
 
71
133
  ```bash
72
134
  pnpm install
73
- pnpm typecheck
74
- pnpm test # node:test + tsx,核心逻辑单测
75
- pnpm build # esbuild 打包成 dist/harness-kit.cjs 单文件可执行
76
- pnpm exec tsx src/cli.ts verify --repo . # 开发态直接跑,且自托管验证本仓
135
+ pnpm typecheck # tsc --noEmit
136
+ pnpm test # node:test + tsx
137
+ pnpm build # esbuild dist/harness-kit.cjs
138
+ pnpm exec tsx src/cli.ts verify --repo . # 自托管验证
77
139
  ```
78
140
 
141
+ ## 文档
142
+
143
+ - [`SPEC-v0.md`](SPEC-v0.md) — manifest schema + 生成/校验契约
144
+
79
145
  ## License
80
146
 
81
147
  MIT © [MeCKodo (二哲)](https://erzhe.me/)
package/SPEC-v0.md CHANGED
@@ -29,7 +29,6 @@
29
29
  <repo>/
30
30
  AGENTS.md # [生成] 跨工具入口,<100 行,指路
31
31
  CLAUDE.md # [生成] 首行 @AGENTS.md
32
- .cursor/rules/00-follow-agents.mdc # [生成] 薄适配器
33
32
  .agents/ # canonical 命名空间(唯一手写源)
34
33
  manifest.yaml # 脊椎(本规范核心,见 §3)
35
34
  knowledge/ # 手写叙述:domain / conventions / journal
@@ -142,7 +141,6 @@ playbooks:
142
141
  generate:
143
142
  - to: "AGENTS.md" render: "identity+capabilities+environment+invariants"
144
143
  - to: "CLAUDE.md" content: "@AGENTS.md"
145
- - to: ".cursor/rules/00-follow-agents.mdc" template: "adapter"
146
144
  ```
147
145
 
148
146
  **保留动词说明**:`capabilities` 的保留动词全部**可选**——它是一套"共享词汇表",仓库只声明适用的子集(库通常没有 `run`;前端 `run` 是 dev server 且 `background: true`)。Agent 冷启动时能预期这些词的语义。
@@ -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(resolve2, reject) {
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
- resolve2(result);
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(resolve2, reject) {
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
- resolve2(result);
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(resolve2) {
11912
+ var p = new Promise(function(resolve3) {
11913
11913
  process.nextTick(function() {
11914
11914
  if (queue.idle()) {
11915
- resolve2();
11915
+ resolve3();
11916
11916
  } else {
11917
11917
  var previousDrain = queue.drain;
11918
11918
  queue.drain = function() {
11919
11919
  if (typeof previousDrain === "function") previousDrain();
11920
- resolve2();
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((resolve2, reject) => {
12406
+ return new Promise((resolve3, reject) => {
12407
12407
  this._stat(filepath, this._fsStatSettings, (error, stats) => {
12408
- return error === null ? resolve2(stats) : reject(error);
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((resolve2, reject) => {
12432
+ return new Promise((resolve3, reject) => {
12433
12433
  this._walkAsync(root, options, (error, entries) => {
12434
12434
  if (error === null) {
12435
- resolve2(entries);
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((resolve2, reject) => {
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", () => resolve2(entries));
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 import_node_path12 = require("node:path");
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");
@@ -16663,8 +16663,7 @@ function validateManifest(m) {
16663
16663
  function renderTargets(m) {
16664
16664
  const targets = [
16665
16665
  ["AGENTS.md", renderAgentsMd(m)],
16666
- ["CLAUDE.md", renderClaudeMd()],
16667
- [".cursor/rules/00-follow-agents.mdc", renderCursorRule()]
16666
+ ["CLAUDE.md", renderClaudeMd()]
16668
16667
  ];
16669
16668
  if (m.routing?.length) targets.push([".agents/routing.md", renderRoutingMd(m)]);
16670
16669
  if (m.modules?.length) targets.push([".agents/modules.md", renderModulesMd(m)]);
@@ -16789,18 +16788,6 @@ function renderClaudeMd() {
16789
16788
  ${GEN_HEADER()}
16790
16789
  `;
16791
16790
  }
16792
- function renderCursorRule() {
16793
- return `---
16794
- description: Follow the repo AGENTS.md (generated from .agents/manifest.yaml)
16795
- alwaysApply: true
16796
- ---
16797
- ${GEN_HEADER()}
16798
-
16799
- Read \`AGENTS.md\` at the repo root for project scope, commands, environment,
16800
- contracts, and invariants. It is the single source of truth (generated).
16801
- Do not edit generated files; edit \`.agents/manifest.yaml\` then run \`harness-kit sync\`.
16802
- `;
16803
- }
16804
16791
 
16805
16792
  // src/state.ts
16806
16793
  var import_node_path5 = require("node:path");
@@ -17164,14 +17151,87 @@ function acceptContractCmd(repo, id) {
17164
17151
  return problems ? 1 : 0;
17165
17152
  }
17166
17153
 
17167
- // src/commands/onboard.ts
17154
+ // src/commands/install-hooks.ts
17155
+ var import_node_child_process4 = require("node:child_process");
17168
17156
  var import_node_fs7 = require("node:fs");
17169
17157
  var import_node_path11 = require("node:path");
17158
+ var MARK = "harness-kit-managed-hook";
17159
+ function hooksDir(repo) {
17160
+ const out = (0, import_node_child_process4.execSync)("git rev-parse --git-path hooks", { cwd: repo, encoding: "utf8" }).trim();
17161
+ return (0, import_node_path11.resolve)(repo, out);
17162
+ }
17163
+ function preCommit() {
17164
+ return `#!/bin/sh
17165
+ # ${MARK} (pre-commit): regenerate agent files from the manifest and stage them,
17166
+ # so generated docs never drift from .agents/manifest.yaml.
17167
+ # Reset with \`harness-kit install-hooks --force\`; delete this file to remove.
17168
+ [ -f .agents/manifest.yaml ] || exit 0
17169
+ HK="\${HARNESS_KIT_CMD:-npx -y @erzhe/harness-kit@latest}"
17170
+ if ! $HK sync --repo . >/dev/null; then
17171
+ echo "harness-kit: sync failed (run \\\`$HK sync\\\` to see why)" >&2
17172
+ exit 1
17173
+ fi
17174
+ git add AGENTS.md CLAUDE.md .agents >/dev/null 2>&1 || true
17175
+ exit 0
17176
+ `;
17177
+ }
17178
+ function prePush() {
17179
+ return `#!/bin/sh
17180
+ # ${MARK} (pre-push): block the push when the agent context has drifted from code.
17181
+ # Bypass once with \`git push --no-verify\`.
17182
+ [ -f .agents/manifest.yaml ] || exit 0
17183
+ HK="\${HARNESS_KIT_CMD:-npx -y @erzhe/harness-kit@latest}"
17184
+ if ! $HK verify --repo .; then
17185
+ echo "" >&2
17186
+ echo "harness-kit: verify failed \u2014 agent context drifted from code." >&2
17187
+ echo " self-heal: run an agent with \\\`$HK onboard\\\` and follow its Maintenance section," >&2
17188
+ echo " then review the diff. Bypass once with \\\`git push --no-verify\\\`." >&2
17189
+ exit 1
17190
+ fi
17191
+ exit 0
17192
+ `;
17193
+ }
17194
+ function installHooksCmd(repo, force = false) {
17195
+ let dir;
17196
+ try {
17197
+ dir = hooksDir(repo);
17198
+ } catch {
17199
+ err("not a git repo (git rev-parse failed) \u2014 run inside a git working tree");
17200
+ return 1;
17201
+ }
17202
+ const targets = [
17203
+ ["pre-commit", preCommit()],
17204
+ ["pre-push", prePush()]
17205
+ ];
17206
+ for (const [name, body] of targets) {
17207
+ const p = (0, import_node_path11.join)(dir, name);
17208
+ if ((0, import_node_fs7.existsSync)(p) && !force) {
17209
+ const cur = (0, import_node_fs7.readFileSync)(p, "utf8");
17210
+ if (!cur.includes(MARK)) {
17211
+ warn(`${name} exists and isn't harness-kit-managed \u2014 skipped (use --force to overwrite)`);
17212
+ continue;
17213
+ }
17214
+ }
17215
+ writeText(p, body);
17216
+ (0, import_node_fs7.chmodSync)(p, 493);
17217
+ ok(`installed ${name}`);
17218
+ }
17219
+ info(`
17220
+ Hooks written to ${dir}`);
17221
+ info("pre-commit: regenerates agent files from the manifest and stages them.");
17222
+ info("pre-push: runs `harness-kit verify` and blocks on drift (bypass: git push --no-verify).");
17223
+ info("override the CLI it calls with env HARNESS_KIT_CMD (e.g. for local dev).");
17224
+ return 0;
17225
+ }
17226
+
17227
+ // src/commands/onboard.ts
17228
+ var import_node_fs8 = require("node:fs");
17229
+ var import_node_path12 = require("node:path");
17170
17230
  var import_node_url = require("node:url");
17171
17231
  var import_meta = {};
17172
17232
  function moduleDir() {
17173
17233
  if (typeof __dirname !== "undefined" && __dirname) return __dirname;
17174
- return (0, import_node_path11.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
17234
+ return (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
17175
17235
  }
17176
17236
  var HERE = moduleDir();
17177
17237
  var SKILL_REL = "skills/erzhe-harness-init/SKILL.md";
@@ -17185,9 +17245,9 @@ Do not fabricate: confirm uncertain fields with the user, and report honest GAPS
17185
17245
 
17186
17246
  `;
17187
17247
  function resolveSkill() {
17188
- for (const up of ["..", (0, import_node_path11.join)("..", "..")]) {
17189
- const p = (0, import_node_path11.join)(HERE, up, SKILL_REL);
17190
- if ((0, import_node_fs7.existsSync)(p)) return p;
17248
+ for (const up of ["..", (0, import_node_path12.join)("..", "..")]) {
17249
+ const p = (0, import_node_path12.join)(HERE, up, SKILL_REL);
17250
+ if ((0, import_node_fs8.existsSync)(p)) return p;
17191
17251
  }
17192
17252
  return null;
17193
17253
  }
@@ -17198,7 +17258,7 @@ function onboardCmd() {
17198
17258
  return 1;
17199
17259
  }
17200
17260
  process.stdout.write(META);
17201
- process.stdout.write((0, import_node_fs7.readFileSync)(p, "utf8"));
17261
+ process.stdout.write((0, import_node_fs8.readFileSync)(p, "utf8"));
17202
17262
  return 0;
17203
17263
  }
17204
17264
 
@@ -17213,13 +17273,14 @@ function guard(fn) {
17213
17273
  }
17214
17274
  }
17215
17275
  var program2 = new Command();
17216
- program2.name("harness-kit").description("AI-friendly repo harness").version("0.1.1");
17217
- var repoOf = (o) => (0, import_node_path12.resolve)(o.repo);
17276
+ program2.name("harness-kit").description("AI-friendly repo harness").version("0.1.3");
17277
+ var repoOf = (o) => (0, import_node_path13.resolve)(o.repo);
17218
17278
  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)));
17219
- 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))));
17279
+ program2.command("sync").description("generate tool files (AGENTS.md, CLAUDE.md) from manifest").option("-C, --repo <dir>", "target repo dir", process.cwd()).action((o) => guard(() => syncCmd(repoOf(o))));
17220
17280
  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))));
17221
17281
  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))));
17222
17282
  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)));
17283
+ 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)));
17223
17284
  program2.command("onboard").description("print the erzhe-harness-init skill for an agent to follow (use via npx, always latest)").action(() => guard(() => onboardCmd()));
17224
17285
  program2.parseAsync();
17225
17286
  /*! Bundled license information:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erzhe/harness-kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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": [
@@ -49,7 +49,7 @@ harness-kit init --repo .
49
49
  ## 3. 生成 + 验证到绿
50
50
 
51
51
  ```
52
- harness-kit sync # manifest -> AGENTS.md / CLAUDE.md / .cursor 规则 / routing.md / modules.md
52
+ harness-kit sync # manifest -> AGENTS.md / CLAUDE.md / routing.md / modules.md
53
53
  harness-kit doctor # 补全度 / 引用路径 / 漂移 / 新鲜度 / 体量预算;按报告修
54
54
  harness-kit verify # 门禁:跑不变量 + 契约 + 漂移,并列出 GAPS
55
55
  ```
@@ -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`,别手改生成物**;把 `harness-kit verify` 挂进 CI / pre-commit 才能真正兑现"跨人跨机一致"。
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