@erzhe/harness-kit 0.1.2 → 0.1.4

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 给 Agent 发一句话,让它帮你把仓库变成 AI 友好的。
4
4
 
5
- harness-kit 把仓库的工程知识(这是什么、怎么跑、什么不能破、改东西看哪)沉淀成一份 `.agents/manifest.yaml`,然后自动生成各家 Agent 的入口文件(`AGENTS.md` / `CLAUDE.md` / Cursor rules),并用门禁保证它们始终一致。
5
+ harness-kit 把仓库的工程知识(这是什么、怎么跑、什么不能破、改东西看哪)沉淀成一份 `.agents/manifest.yaml`,然后自动生成各家 Agent 的入口文件(`AGENTS.md` / `CLAUDE.md`),并用门禁保证它们始终一致。
6
6
 
7
7
  ---
8
8
 
@@ -20,6 +20,7 @@ Onboard 本仓库到 harness-kit:跑 npx -y @erzhe/harness-kit@latest onboard
20
20
  2. 扫描你的仓库(读 README、package.json、目录结构…)
21
21
  3. 逐块跟你确认着填 `.agents/manifest.yaml`
22
22
  4. 跑 `sync` / `doctor` / `verify` 直到全绿
23
+ 5. 装上 git 钩子(`install-hooks`):之后 pre-commit 自动 sync、pre-push verify 拦漂移,维护不用你记命令
23
24
 
24
25
  全程走 `npx`,不往机器上装任何全局东西;你一发新版,所有人下次执行就用上了。
25
26
 
@@ -31,7 +32,6 @@ Onboard 本仓库到 harness-kit:跑 npx -y @erzhe/harness-kit@latest onboard
31
32
  你的仓库/
32
33
  AGENTS.md ← [生成] 跨工具入口,Agent 每次会话必读
33
34
  CLAUDE.md ← [生成] Claude Code 入口
34
- .cursor/rules/ ← [生成] Cursor 规则
35
35
  .agents/
36
36
  manifest.yaml ← 你维护的唯一真相源
37
37
  knowledge/ ← Agent 从代码推不出来的知识(领域、约定、决策)
@@ -45,13 +45,29 @@ Onboard 本仓库到 harness-kit:跑 npx -y @erzhe/harness-kit@latest onboard
45
45
 
46
46
  ---
47
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。
55
+
56
+ ```
57
+ 代码演进 → [hook/CI] verify 红灯,指出哪块过期/哪个契约变了
58
+ → [Agent] 自动修 + 产出变更清单
59
+ → [你] review:批准 or 打回 ← 唯一的人工动作
60
+ ```
61
+
62
+ ---
63
+
48
64
  ## 命令
49
65
 
50
66
  | 命令 | 作用 |
51
67
  | --- | --- |
52
68
  | `harness-kit onboard` | 打印 onboard skill 给 Agent(配合 npx,永远最新、零安装) |
53
69
  | `harness-kit init` | 铺 `.agents/` 骨架 + 空白 manifest |
54
- | `harness-kit sync` | manifest → 生成 AGENTS.md / CLAUDE.md / Cursor rules / routing / modules |
70
+ | `harness-kit sync` | manifest → 生成 AGENTS.md / CLAUDE.md / routing / modules |
55
71
  | `harness-kit doctor` | 体检:完整性 / 路径引用 / 漂移 / 新鲜度 / 体量预算 |
56
72
  | `harness-kit verify` | 门禁:跑不变量 + 契约 + 漂移,列出 GAPS,失败非 0 退出 |
57
73
  | `harness-kit accept-contract` | 有意变更接口后,记录新的契约指纹为基线 |
@@ -82,7 +98,7 @@ harness-kit 把这些沉淀进 manifest,再确定性地生成与校验。
82
98
 
83
99
  harness-kit sync (确定性生成,勿手改产物)
84
100
 
85
- AGENTS.md / CLAUDE.md / .cursor/rules / .agents/routing.md / .agents/modules.md
101
+ AGENTS.md / CLAUDE.md / .agents/routing.md / .agents/modules.md
86
102
 
87
103
  harness-kit verify (门禁:不变量 + 契约 + 漂移)
88
104
 
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 冷启动时能预期这些词的语义。
@@ -16619,9 +16619,24 @@ function loadManifest(repo) {
16619
16619
  }
16620
16620
  function validateManifest(m) {
16621
16621
  const issues = [];
16622
+ function checkStrArr(arr, prefix) {
16623
+ if (!arr) return;
16624
+ for (let i = 0; i < arr.length; i++) {
16625
+ if (typeof arr[i] !== "string") {
16626
+ issues.push({
16627
+ level: "error",
16628
+ msg: `${prefix}[${i}] \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5F53\u524D\u662F ${typeof arr[i]}\uFF09`
16629
+ });
16630
+ }
16631
+ }
16632
+ }
16622
16633
  if (!m.spec) issues.push({ level: "error", msg: "\u7F3A\u5C11 spec \u5B57\u6BB5" });
16623
16634
  if (!m.identity?.name) issues.push({ level: "error", msg: "identity.name \u5FC5\u586B" });
16624
16635
  if (!m.identity?.summary) issues.push({ level: "warn", msg: "identity.summary \u5EFA\u8BAE\u586B\u5199" });
16636
+ checkStrArr(m.identity?.scope_in, "identity.scope_in");
16637
+ checkStrArr(m.identity?.scope_out, "identity.scope_out");
16638
+ checkStrArr(m.identity?.upstream, "identity.upstream");
16639
+ checkStrArr(m.identity?.downstream, "identity.downstream");
16625
16640
  for (const [verb, cap] of Object.entries(m.capabilities ?? {})) {
16626
16641
  if (!cap?.run) issues.push({ level: "error", msg: `capabilities.${verb}.run \u5FC5\u586B` });
16627
16642
  }
@@ -16636,11 +16651,23 @@ function validateManifest(m) {
16636
16651
  if (!inv.enforcement && !inv.check && !inv.manual) {
16637
16652
  issues.push({ level: "warn", msg: `invariant ${inv.id} \u65E2\u65E0 enforcement/check \u4E5F\u672A\u6807 manual` });
16638
16653
  }
16654
+ const pfx = `invariant "${inv.id}" \u7684 enforcement`;
16655
+ checkStrArr(inv.enforcement?.forbid_pattern, `${pfx}.forbid_pattern`);
16656
+ checkStrArr(inv.enforcement?.forbid_import, `${pfx}.forbid_import`);
16657
+ checkStrArr(inv.enforcement?.require_pattern, `${pfx}.require_pattern`);
16658
+ checkStrArr(inv.enforcement?.path_glob, `${pfx}.path_glob`);
16659
+ }
16660
+ for (const k of m.knowledge ?? []) {
16661
+ checkStrArr(k.binds, `knowledge "${k.path}" \u7684 binds`);
16639
16662
  }
16640
16663
  const capVerbs = new Set(Object.keys(m.capabilities ?? {}));
16641
16664
  for (const r of m.routing ?? []) {
16642
16665
  if (!r.when) issues.push({ level: "error", msg: "\u5B58\u5728\u7F3A\u5C11 when \u7684 routing \u6761\u76EE" });
16666
+ for (const field of ["read", "entry", "dont_assume", "verify"]) {
16667
+ checkStrArr(r[field], `routing "${r.when}" \u7684 ${field}`);
16668
+ }
16643
16669
  for (const v of r.verify ?? []) {
16670
+ if (typeof v !== "string") continue;
16644
16671
  if (!v.includes(" ") && !capVerbs.has(v))
16645
16672
  issues.push({ level: "warn", msg: `routing "${r.when}" \u7684 verify \u5F15\u7528\u4E86\u672A\u58F0\u660E\u7684 capability: ${v}` });
16646
16673
  }
@@ -16655,16 +16682,24 @@ function validateManifest(m) {
16655
16682
  modSeen.add(mod.name);
16656
16683
  if (!mod.entry?.length)
16657
16684
  issues.push({ level: "warn", msg: `module ${mod.name} \u672A\u58F0\u660E entry\uFF08\u65E0\u6CD5\u505A\u65B0\u9C9C\u5EA6\u7ED1\u5B9A\uFF09` });
16685
+ for (const field of ["entry", "upstream", "downstream", "must_know", "pitfalls"]) {
16686
+ checkStrArr(mod[field], `module "${mod.name}" \u7684 ${field}`);
16687
+ }
16658
16688
  }
16659
16689
  return issues;
16660
16690
  }
16661
16691
 
16662
16692
  // src/render.ts
16693
+ function safeStr(v) {
16694
+ if (typeof v === "string") return v;
16695
+ if (v == null) return "";
16696
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
16697
+ return JSON.stringify(v);
16698
+ }
16663
16699
  function renderTargets(m) {
16664
16700
  const targets = [
16665
16701
  ["AGENTS.md", renderAgentsMd(m)],
16666
- ["CLAUDE.md", renderClaudeMd()],
16667
- [".cursor/rules/00-follow-agents.mdc", renderCursorRule()]
16702
+ ["CLAUDE.md", renderClaudeMd()]
16668
16703
  ];
16669
16704
  if (m.routing?.length) targets.push([".agents/routing.md", renderRoutingMd(m)]);
16670
16705
  if (m.modules?.length) targets.push([".agents/modules.md", renderModulesMd(m)]);
@@ -16700,15 +16735,15 @@ function renderAgentsMd(m) {
16700
16735
  L.push("");
16701
16736
  if (id.scope_in?.length || id.scope_out?.length) {
16702
16737
  L.push("## Scope");
16703
- if (id.scope_in?.length) L.push("", "In scope:", ...id.scope_in.map((s) => `- ${s}`));
16738
+ if (id.scope_in?.length) L.push("", "In scope:", ...id.scope_in.map((s) => `- ${safeStr(s)}`));
16704
16739
  if (id.scope_out?.length)
16705
- L.push("", "Out of scope (do NOT modify here):", ...id.scope_out.map((s) => `- ${s}`));
16740
+ L.push("", "Out of scope (do NOT modify here):", ...id.scope_out.map((s) => `- ${safeStr(s)}`));
16706
16741
  L.push("");
16707
16742
  }
16708
16743
  if (id.upstream?.length || id.downstream?.length) {
16709
16744
  L.push("## Dependencies");
16710
- if (id.upstream?.length) L.push("", `Upstream: ${id.upstream.join(", ")}`);
16711
- if (id.downstream?.length) L.push("", `Downstream: ${id.downstream.join(", ")}`);
16745
+ if (id.upstream?.length) L.push("", `Upstream: ${id.upstream.map(safeStr).join(", ")}`);
16746
+ if (id.downstream?.length) L.push("", `Downstream: ${id.downstream.map(safeStr).join(", ")}`);
16712
16747
  L.push("");
16713
16748
  }
16714
16749
  const caps = Object.entries(m.capabilities ?? {});
@@ -16760,10 +16795,10 @@ function renderRoutingMd(m) {
16760
16795
  L.push("Find the change-type that matches your task and follow that row before editing.", "");
16761
16796
  for (const r of m.routing ?? []) {
16762
16797
  L.push(`## ${r.when}`);
16763
- if (r.read?.length) L.push(`- Read first: ${r.read.map((s) => `\`${s}\``).join(", ")}`);
16764
- if (r.entry?.length) L.push(`- Entry points: ${r.entry.map((s) => `\`${s}\``).join(", ")}`);
16765
- if (r.dont_assume?.length) for (const d of r.dont_assume) L.push(`- Do NOT assume: ${d}`);
16766
- if (r.verify?.length) L.push(`- Minimum verification: ${r.verify.map((s) => `\`${s}\``).join(", ")}`);
16798
+ if (r.read?.length) L.push(`- Read first: ${r.read.map((s) => `\`${safeStr(s)}\``).join(", ")}`);
16799
+ if (r.entry?.length) L.push(`- Entry points: ${r.entry.map((s) => `\`${safeStr(s)}\``).join(", ")}`);
16800
+ if (r.dont_assume?.length) for (const d of r.dont_assume) L.push(`- Do NOT assume: ${safeStr(d)}`);
16801
+ if (r.verify?.length) L.push(`- Minimum verification: ${r.verify.map((s) => `\`${safeStr(s)}\``).join(", ")}`);
16767
16802
  L.push("");
16768
16803
  }
16769
16804
  return L.join("\n");
@@ -16774,11 +16809,11 @@ function renderModulesMd(m) {
16774
16809
  L.push("# Module map", "");
16775
16810
  for (const mod of m.modules ?? []) {
16776
16811
  L.push(`## ${mod.name} \u2014 ${mod.role}`);
16777
- if (mod.entry?.length) L.push(`- Entry: ${mod.entry.map((s) => `\`${s}\``).join(", ")}`);
16778
- if (mod.upstream?.length) L.push(`- Upstream: ${mod.upstream.join(", ")}`);
16779
- if (mod.downstream?.length) L.push(`- Downstream: ${mod.downstream.join(", ")}`);
16780
- if (mod.must_know?.length) for (const k of mod.must_know) L.push(`- Must know: ${k}`);
16781
- if (mod.pitfalls?.length) for (const p of mod.pitfalls) L.push(`- Pitfall: ${p}`);
16812
+ if (mod.entry?.length) L.push(`- Entry: ${mod.entry.map((s) => `\`${safeStr(s)}\``).join(", ")}`);
16813
+ if (mod.upstream?.length) L.push(`- Upstream: ${mod.upstream.map(safeStr).join(", ")}`);
16814
+ if (mod.downstream?.length) L.push(`- Downstream: ${mod.downstream.map(safeStr).join(", ")}`);
16815
+ if (mod.must_know?.length) for (const k of mod.must_know) L.push(`- Must know: ${safeStr(k)}`);
16816
+ if (mod.pitfalls?.length) for (const p of mod.pitfalls) L.push(`- Pitfall: ${safeStr(p)}`);
16782
16817
  L.push("");
16783
16818
  }
16784
16819
  return L.join("\n");
@@ -16789,18 +16824,6 @@ function renderClaudeMd() {
16789
16824
  ${GEN_HEADER()}
16790
16825
  `;
16791
16826
  }
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
16827
 
16805
16828
  // src/state.ts
16806
16829
  var import_node_path5 = require("node:path");
@@ -17184,7 +17207,7 @@ if ! $HK sync --repo . >/dev/null; then
17184
17207
  echo "harness-kit: sync failed (run \\\`$HK sync\\\` to see why)" >&2
17185
17208
  exit 1
17186
17209
  fi
17187
- git add AGENTS.md CLAUDE.md .cursor/rules .agents >/dev/null 2>&1 || true
17210
+ git add AGENTS.md CLAUDE.md .agents >/dev/null 2>&1 || true
17188
17211
  exit 0
17189
17212
  `;
17190
17213
  }
@@ -17286,10 +17309,10 @@ function guard(fn) {
17286
17309
  }
17287
17310
  }
17288
17311
  var program2 = new Command();
17289
- program2.name("harness-kit").description("AI-friendly repo harness").version("0.1.2");
17312
+ program2.name("harness-kit").description("AI-friendly repo harness").version("0.1.3");
17290
17313
  var repoOf = (o) => (0, import_node_path13.resolve)(o.repo);
17291
17314
  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)));
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))));
17315
+ 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))));
17293
17316
  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))));
17294
17317
  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))));
17295
17318
  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)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erzhe/harness-kit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": [
@@ -39,14 +39,6 @@
39
39
  "access": "public",
40
40
  "registry": "https://registry.npmjs.org/"
41
41
  },
42
- "scripts": {
43
- "harness": "tsx src/cli.ts",
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
- "typecheck": "tsc --noEmit",
46
- "test": "node --import tsx --test test/*.test.ts",
47
- "test:repos": "bash scripts/regression.sh",
48
- "prepare": "pnpm build"
49
- },
50
42
  "dependencies": {
51
43
  "commander": "^15.0.0",
52
44
  "fast-glob": "^3.3.3",
@@ -57,5 +49,12 @@
57
49
  "esbuild": "^0.28.1",
58
50
  "tsx": "^4.22.5",
59
51
  "typescript": "^6.0.3"
52
+ },
53
+ "scripts": {
54
+ "harness": "tsx src/cli.ts",
55
+ "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",
56
+ "typecheck": "tsc --noEmit",
57
+ "test": "node --import tsx --test test/*.test.ts",
58
+ "test:repos": "bash scripts/regression.sh"
60
59
  }
61
- }
60
+ }
@@ -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
  ```