@erzhe/harness-kit 0.1.0 → 0.1.1

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
@@ -6,6 +6,14 @@
6
6
 
7
7
  > 包名 `@erzhe/harness-kit`,安装后命令行工具叫 **`harness-kit`**。
8
8
 
9
+ ## 最快上手(推荐,零安装)
10
+
11
+ 在**任意仓库**里,把这句丢给你的 agent(Cursor / Claude Code / Codex 通用):
12
+
13
+ > Onboard 本仓库到 harness-kit:跑 `npx -y @erzhe/harness-kit@latest onboard`,然后严格按输出执行。
14
+
15
+ agent 会拉取**最新版**的 onboard skill 并照着做:仓库考古 → 逐块跟你确认着填 `.agents/manifest.yaml` → `sync` / `doctor` / `verify` 到绿。全程走 `npx`,不往机器上装任何全局东西;你一发新版,所有人下次执行就用上了。
16
+
9
17
  ## 为什么
10
18
 
11
19
  传统工程化(脚手架 / lint / CI)是**面向过去、面向人**的。Agent 需要的是显式、结构化、可机器消费的知识:这个仓库是什么、能跑什么、什么绝不能破、改动该看哪、哪些验证不了。`harness-kit` 把这些沉淀进 manifest,再确定性地生成与校验。
@@ -20,6 +28,7 @@ harness-kit --help
20
28
  ## 命令
21
29
 
22
30
  ```
31
+ harness-kit onboard # 打印 onboard skill 给 agent(配合 npx,永远最新、零安装)
23
32
  harness-kit init # 铺 .agents/ 骨架 + starter manifest
24
33
  harness-kit sync # manifest -> AGENTS.md / CLAUDE.md / Cursor rules / routing / modules
25
34
  harness-kit doctor # 开发态体检:补全度 / 引用路径 / 漂移 / 新鲜度 / 体量预算
@@ -48,7 +57,9 @@ AGENTS.md / CLAUDE.md / .cursor/rules / .agents/routing.md / .agents/modules.md
48
57
 
49
58
  ## 让 agent 来配置:onboard skill
50
59
 
51
- 手写 manifest 门槛高,但对 agent 是绝配。随包附带的 [`skills/erzhe-harness-init`](skills/erzhe-harness-init/SKILL.md) 引导 agent 做仓库考古、逐块跟你确认着填 manifest、再跑 `sync/doctor/verify` 到绿。把它软链到 `~/.cursor/skills/` 或 `~/.claude/skills/` 即可被发现。
60
+ 手写 manifest 门槛高,但对 agent 是绝配。随包附带的 [`skills/erzhe-harness-init`](skills/erzhe-harness-init/SKILL.md) 引导 agent 做仓库考古、逐块跟你确认着填 manifest、再跑 `sync/doctor/verify` 到绿。
61
+
62
+ 最省事的用法就是上面的 [最快上手](#最快上手推荐零安装)——`npx ... onboard` 把 skill 打印给 agent,零安装、永远最新。若想让某个 agent **自动触发**(不用每次点名),可选地把 skill 软链进它的 skill 目录:`ln -sf "$(npm root -g)/@erzhe/harness-kit/skills/erzhe-harness-init" ~/.cursor/skills/`。
52
63
 
53
64
  ## 文档
54
65
 
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.check` + `capabilities.verify` + "生成物无漂移"。任一失败即 fail。
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` 看退出码。
@@ -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 statSync(path2, optionsOrSettings) {
11217
+ function statSync3(path2, optionsOrSettings) {
11218
11218
  const settings = getSettings(optionsOrSettings);
11219
11219
  return sync.read(path2, settings);
11220
11220
  }
11221
- exports2.statSync = statSync;
11221
+ exports2.statSync = statSync3;
11222
11222
  function getSettings(settingsOrOptions = {}) {
11223
11223
  if (settingsOrOptions instanceof settings_1.default) {
11224
11224
  return settingsOrOptions;
@@ -16471,7 +16471,7 @@ function useColor() {
16471
16471
  var program = new Command();
16472
16472
 
16473
16473
  // src/cli.ts
16474
- var import_node_path11 = require("node:path");
16474
+ var import_node_path12 = 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
- if (!(0, import_node_fs2.existsSync)(absPath)) return null;
16489
- return sha256((0, import_node_fs2.readFileSync)(absPath));
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
- if ((0, import_node_fs5.existsSync)((0, import_node_path7.join)(repo, rel))) ok(`${label}: ${rel}`);
16872
- else {
16873
- warn(`${label} points at a missing path: ${rel}`);
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 ?? []) checkPath(k.path, "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 import_fast_glob = __toESM(require_out4(), 1);
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 = import_fast_glob.default.sync(globs, {
16992
+ const files = import_fast_glob2.default.sync(globs, {
16971
16993
  cwd: repo,
16972
16994
  onlyFiles: true,
16973
16995
  dot: false,
@@ -17142,6 +17164,44 @@ function acceptContractCmd(repo, id) {
17142
17164
  return problems ? 1 : 0;
17143
17165
  }
17144
17166
 
17167
+ // src/commands/onboard.ts
17168
+ var import_node_fs7 = require("node:fs");
17169
+ var import_node_path11 = require("node:path");
17170
+ var import_node_url = require("node:url");
17171
+ var import_meta = {};
17172
+ function moduleDir() {
17173
+ if (typeof __dirname !== "undefined" && __dirname) return __dirname;
17174
+ return (0, import_node_path11.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
17175
+ }
17176
+ var HERE = moduleDir();
17177
+ var SKILL_REL = "skills/erzhe-harness-init/SKILL.md";
17178
+ var META = `<!-- harness-kit onboard -->
17179
+ You are an AI agent onboarding the CURRENT repository to harness-kit.
17180
+ Follow the skill below verbatim. Run every harness-kit command through
17181
+ \`npx -y @erzhe/harness-kit@latest <cmd>\` \u2014 always the latest version, no global install.
17182
+ Do not fabricate: confirm uncertain fields with the user, and report honest GAPS.
17183
+
17184
+ ---
17185
+
17186
+ `;
17187
+ 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;
17191
+ }
17192
+ return null;
17193
+ }
17194
+ function onboardCmd() {
17195
+ const p = resolveSkill();
17196
+ if (!p) {
17197
+ err(`bundled skill not found (${SKILL_REL})`);
17198
+ return 1;
17199
+ }
17200
+ process.stdout.write(META);
17201
+ process.stdout.write((0, import_node_fs7.readFileSync)(p, "utf8"));
17202
+ return 0;
17203
+ }
17204
+
17145
17205
  // src/cli.ts
17146
17206
  function guard(fn) {
17147
17207
  try {
@@ -17153,13 +17213,14 @@ function guard(fn) {
17153
17213
  }
17154
17214
  }
17155
17215
  var program2 = new Command();
17156
- program2.name("harness-kit").description("AI-friendly repo harness").version("0.1.0");
17157
- var repoOf = (o) => (0, import_node_path11.resolve)(o.repo);
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);
17158
17218
  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
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))));
17160
17220
  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
17221
  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
17222
  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)));
17223
+ program2.command("onboard").description("print the erzhe-harness-init skill for an agent to follow (use via npx, always latest)").action(() => guard(() => onboardCmd()));
17163
17224
  program2.parseAsync();
17164
17225
  /*! Bundled license information:
17165
17226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erzhe/harness-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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
- 先跑 `harness-kit --help` 确认可用。
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`(入口文件,也用于新鲜度绑定)、`upstream` / `downstream`、`must_know`、`pitfalls`(常见坑——最高价值的一栏)。
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