signoff-cli 0.0.0-stage → 4.0.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.
Files changed (57) hide show
  1. package/.pre-commit-hooks.yaml +27 -0
  2. package/CHANGELOG.md +344 -0
  3. package/LICENSE +21 -0
  4. package/README.md +540 -3
  5. package/action.yml +24 -0
  6. package/bin/signoff.js +218 -0
  7. package/docs/diagrams/README.md +41 -0
  8. package/docs/diagrams/signoff-1-capability-en.png +0 -0
  9. package/docs/diagrams/signoff-1-capability.png +0 -0
  10. package/docs/diagrams/signoff-2-market-en.png +0 -0
  11. package/docs/diagrams/signoff-2-market.png +0 -0
  12. package/docs/diagrams/signoff-3-user-en.png +0 -0
  13. package/docs/diagrams/signoff-3-user.png +0 -0
  14. package/docs/diagrams/signoff-4-flow-en.png +0 -0
  15. package/docs/diagrams/signoff-4-flow.png +0 -0
  16. package/docs/diagrams/signoff-5-competitors-en.png +0 -0
  17. package/docs/diagrams/signoff-5-competitors.png +0 -0
  18. package/docs/diagrams/src/signoff-1-capability-en.mmd +81 -0
  19. package/docs/diagrams/src/signoff-1-capability.mmd +84 -0
  20. package/docs/diagrams/src/signoff-2-market-en.mmd +72 -0
  21. package/docs/diagrams/src/signoff-2-market.mmd +72 -0
  22. package/docs/diagrams/src/signoff-3-user-en.mmd +94 -0
  23. package/docs/diagrams/src/signoff-3-user.mmd +92 -0
  24. package/docs/gates.en.md +105 -0
  25. package/docs/gates.md +88 -0
  26. package/docs/integrations.md +58 -0
  27. package/docs/promotion.md +71 -0
  28. package/docs/signoff.schema.json +108 -0
  29. package/docs/trust.md +72 -0
  30. package/gates/guard.py +474 -0
  31. package/gates/p1_verify.py +111 -0
  32. package/gates/p2_coverage.py +127 -0
  33. package/gates/p3_queue.py +186 -0
  34. package/gates/p4_ledger.py +249 -0
  35. package/gates/p5_slop.py +109 -0
  36. package/lib/audit.js +213 -0
  37. package/lib/bootstrap.js +361 -0
  38. package/lib/checkpoint.js +184 -0
  39. package/lib/ci.js +58 -0
  40. package/lib/core.js +120 -0
  41. package/lib/cov.js +253 -0
  42. package/lib/detect.js +169 -0
  43. package/lib/doctor.js +304 -0
  44. package/lib/editor.js +97 -0
  45. package/lib/explain.js +73 -0
  46. package/lib/guard.js +1229 -0
  47. package/lib/p1.js +469 -0
  48. package/lib/p2.js +323 -0
  49. package/lib/p3.js +633 -0
  50. package/lib/p4.js +733 -0
  51. package/lib/p5.js +438 -0
  52. package/lib/preset.js +66 -0
  53. package/lib/secrets.js +336 -0
  54. package/lib/sign.js +196 -0
  55. package/package.json +56 -6
  56. package/test/selftest.js +1227 -0
  57. package/test/selftest.py +190 -0
package/README.md CHANGED
@@ -1,3 +1,540 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # 落印 / SignOff
2
+
3
+ > **包名 4.0.0 起为 `signoff-cli`**(曾用名 `dsh-signoff`,旧包已标记 deprecated)。
4
+ > 命令仍是 `signoff`;仓库地址仍是 `ayuanxx-max/dsh-signoff`。DSH 只是它最早的使用场景,
5
+ > 任意 git 仓库、任意语言都可以直接用。
6
+
7
+ > 中文名「落印」:印落纸面才算数,对应英文名 SignOff。曾用名「一诺过五关」「执印」。命令 `signoff`、包名 `dsh-signoff` 不变。
8
+
9
+ > 签下的那句“做完了”,得自己走过五道关才算数。
10
+
11
+ 五道关卡,五条命令,退出码即判决。每一关都对应 agent 交付时的一种失败方式:靠记忆跑测试、把“有开发页”当验证、多个 agent 撞同一文件、危险动作做过就忘、文档里塞空话。SignOff 把这五种失败变成五个可执行的检查。
12
+
13
+ ```bash
14
+ npm i -g signoff-cli # 或 npx signoff-cli
15
+ signoff p1 # 一关一关过
16
+ signoff all # p1 + p2 + p5 + p4 verify
17
+ ```
18
+
19
+ | 关 | 命令 | 判决什么 |
20
+ | --- | --- | --- |
21
+ | P1 验证 | `signoff p1` | 检查是否真的跑过、跑在预算内、用例数多少 |
22
+ | P2 覆盖 | `signoff p2` | 验证到了哪一级:scenario / harness / smoke / missing |
23
+ | P3 队列 | `signoff p3 validate\|plan\|collect` | 卡片是否齐、并发是否撞、谁越界谁没交 |
24
+ | P4 账本 | `signoff p4 ask\|confirm\|verify\|export` | 危险动作有没有票、回滚点是否还在 |
25
+ | 自查 | `signoff doctor [--json]` | 为什么没按预期生效:钩子、PATH、版本、安装点、账本、装钩子之后的缺失提交 |
26
+ | P5 反空话 | `signoff p5` | 文档里有没有空话、`绝对`化断言、无数字的`已验证` |
27
+
28
+ ## 一张图看懂
29
+
30
+ ![SignOff 完整能力与关系](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-1-capability.png)
31
+
32
+ 六个层次从左到右:**入口**(CLI / git 钩子 / CI / Agent 面)都走同一个 `guard.js`;**调度**决定跑哪几关、
33
+ 谁算通过;**五关**每关都写清“读什么、判什么、怎么修”;**状态机**说明一次提交里 marker 与 trailer 是怎么产生的;
34
+ **产物**是判断留下的证据;最右是**外部依赖**——只有 Node ≥18 与 git 是硬要求,其余都可缺。
35
+ 判定没有“部分通过”:退出码 0 通过、1 任一关判红、2 用法或环境错。
36
+
37
+ ![用户路径:安装、使用、排错](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-3-user.png)
38
+
39
+ 上半部分按 0→5 的先后排:前置检查 → 安装 → 初始化 → 日常提交 → 红了怎么办 → 升级与卸载。
40
+ 红框是必做,黄框是最容易踩的坑(`--chain`、`--lock-test`、npm proxy)。
41
+ 下半部分三块是“不改你的流程、但会改变你看到的现象”:平台差异、环境差异,以及三条红线
42
+ (`--no-verify`、`SIGNOFF_SKIP=1`、调阈值让红变绿)。
43
+
44
+ 两张图都由 Mermaid 生成,源文件在 [`docs/diagrams/src/`](docs/diagrams/src/),改文案重渲一次即可,
45
+ 不用重画。
46
+
47
+ ## 零接触:装一次,之后不用管
48
+
49
+ 目标是一条命令之后不再需要任何操作:`npm i -g signoff-cli` 装完即生效,之后每次 `git commit` 都自动受五关约束。
50
+
51
+ | 层 | 什么时候发生 | 覆盖范围 |
52
+ | --- | --- | --- |
53
+ | 机器层 | `npm i -g` 的 postinstall 写 `core.hooksPath` → `~/.signoff/hooks` | 本机**所有**仓库,新克隆的也算 |
54
+ | 仓库层 | postinstall(在某个仓库里执行时)或 `signoff ensure` | 该仓库的 `.git/hooks`、`.signoff/config.json`、CI workflow、ledger |
55
+ | 仓库声明 | 仓库根提交了 `signoff.json`(`signoff init --share` 生成) | 任何克隆:机器层见到声明就自动装全五关,克隆方零操作 |
56
+ | 关内 | 每次 commit | 已 opt-in 的仓库跑 P1–P5;未 opt-in 的仓库只跑 P1(测试必须过),不写任何状态 |
57
+
58
+ ```bash
59
+ npm i -g signoff-cli # 一次;postinstall 自动装好机器层 + 当前仓库
60
+ signoff ensure # 换个仓库想把五关都装上,跑这一条(幂等)
61
+ signoff ensure --global # 只重装/修复机器层
62
+ signoff ensure --json # 给脚本看的结构化结果
63
+ ```
64
+
65
+ npm 11 会把 `postinstall` 放进 allowlist 提示,可能不执行——所以还有一层兜底:**在该仓库里第一次跑任何一关**(比如 `signoff p5 README.md`)会自动把五关装好。
66
+ 要放行安装脚本就加 `npm i -g signoff-cli --allow-scripts=dsh-signoff`。
67
+
68
+ 行为约定:
69
+
70
+ - 已 opt-in(有 `.signoff/config.json`):pre-commit 跑全套五关,PASS 才写 marker 和 trailer。
71
+ - 未 opt-in:只跑 P1(有测试才跑,没测试就明说"没检测到测试运行器"后放行),不写 `.signoff` 配置、不带 trailer。
72
+ - 仓库自带的钩子不会被绕过:机器层先执行 `.git/hooks/<stage>`,它若是 signoff 的钩子就直接以它的判决为准。
73
+ - 提交 `signoff.json` 等于「这个仓库同意 P1–P5」:`.signoff/` 是 gitignore 的,声明文件进 git,
74
+ 所以别人克隆下来第一次 commit 就会把 hooks / config / CI 自己补上。字段见 `docs/signoff.schema.json`。
75
+ - 临时退出:`SIGNOFF_NO_BOOTSTRAP=1`(不做自动安装)、`SIGNOFF_NO_MACHINE=1`(不跑机器层的轻量关)。
76
+ - 撤销机器层:`signoff uninstall --global`。
77
+
78
+ ## 一、案例:本次 DSH 2.0.22-next升级后在修复(dsh-session-fork-tree 0.9.34)bug里,五关各自做了什么?
79
+
80
+ 这次修的是“升级 DSH 后,点侧栏「···」菜单,菜单闪一下就消失”。五关实际起了什么作用,逐条列出,包括没发挥作用的那一关。
81
+
82
+ | 关 | 本轮实际动作 | 证据 | 判定 |
83
+ | --- | --- | --- | --- |
84
+ | P1 | 手动跑 `precommit.ps1`;闸门第 10 层 pytest | `PASS 3666 ms`(101 用例);第 10 层 `3438 ms` | 护栏有效;但它只跑工具自身的 Python 用例,**抓不到这次的前端问题**,也**没能自动拦提交**(插件目录是独立 git 仓库、没装钩子) |
85
+ | P2 | 三套真实 DOM 场景:`menu`(11) / `arrange`(13) / `perf`(6);并把该包纳入覆盖率账本 | `coverage.py`:apps 4→5,scenario 5/5,断言 36→77 | **主力**。决定“修好没有”的是场景断言,不是肉眼;账本让这些断言不再是一次性证据 |
86
+ | P3 | 未使用:没写任务卡,没跑 `fanout.py` | 无 `card.json`、无 wave 记录 | **未发挥**。三件事有依赖顺序(修→测→说明引用实测数字),但这不等于可以不写卡留痕 |
87
+ | P4 | 事后审计抓到我自己填的假回滚点;补录发布记录 | `verify` 先 FAIL:`rollback point 'npm deprecate …' not a git ref`;修复后 `{"ok": true, "entries": 4, "problems": []}` | **事后有效,事前缺位**。`npm publish`、GitHub Release、改 profile 都是先做后记,没走取票 |
88
+ | P5 | 三份新文档过 slop 六规则;覆盖率表由工具重生成 | slop `{"ok": true, "files": 3, "hits": []}` | 有效;但把包登记进覆盖率是**发布之后**补的,说明里那组断言数当时还没进账本 |
89
+
90
+ 一句话:**P2 决定“修好没有”,P5 决定“我有没有说大话”,P1 只证明没弄坏别处,P4 事后抓住了我自己的一笔脏账,P3 全程没上场。** 这个结论本身就是 SignOff 想要的输出:每一关都要能回答“发挥了没有”,不能只说“流程走过了”。
91
+
92
+ <h2 style="color:#c00000">你不需要做什么,照常进行任务,由它来守护这个过程;</h2>
93
+
94
+ ## 二、自动守护:装一次,之后不用记
95
+
96
+ `signoff init` 把五关接进 git 与 CI,一次安装,之后正常 `git commit` / `git push` / 开 PR 都由它先查:
97
+
98
+ | 安装物 | 作用 |
99
+ | --- | --- |
100
+ | `.git/hooks/pre-commit` | 暂存区一变就跑闸门:文档改动 → P5;能探到测试命令 → P1;有 `verify/scenarios/` → P2 `--strict`;有账本 → P4 `verify`。红了直接拒绝提交 |
101
+ | `.git/hooks/pre-push` | push 前跑同一套(`--full`,不吃缓存) |
102
+ | `.git/hooks/commit-msg` | 把 `Signoff-Pass: <tree>`(跳过时 `Signoff-Skip:`)写进提交信息;`signoff audit` 与 CI 据此判断这次提交真的过过闸门 |
103
+ | `.github/workflows/signoff.yml` | 每个 push / PR 跑 `npx signoff-cli guard --ci`,绕不过去 |
104
+ | `.signoff/config.json` | 测试命令、预算、文档目录;`signoff status` 随时查看 |
105
+ | `.gitignore` | 追加 `.signoff/`,结果不进版本库 |
106
+
107
+ 只依赖 **Node 18+**:闸门本身是 Node 实现(`lib/p*.js`),没有 Python 也能跑完整五关,不存在降级模式。
108
+
109
+ 三条稳定性措施:**同一份暂存内容只判一次**(命中缓存约 0 ms,反复 `git commit --amend` 不重复付时间);**失败只打印失败那一关的尾部**(完整日志在 `.signoff/hook.log`);**超预算直接判红**(默认 180 秒,`--budget` 可改)。
110
+
111
+ **绕过的路仍然有,但不再无痕**:`signoff audit --range origin/main..HEAD` 逐个提交核对标记与提交树——缺失就是绕过(`git commit --no-verify`),判红;`SIGNOFF_SKIP=1` 的提交写成 `Signoff-Skip:` 并记进 `.signoff/skips.log`,`--strict` 下同样判红。生成的 CI 里已经带这一步。
112
+
113
+ **队友为什么也会被守住**:先把 `dsh-signoff` 放进 `devDependencies`(`npm i -D signoff-cli`),再 `signoff init --prepare`,它会把 `signoff init --quiet` 并进 `package.json` 的 `prepare`,队友 `npm install` 完钩子就有了。没这个依赖时 `--prepare` 会拒绝写入并提示——否则 `npm install` 会因为找不到 CLI 直接失败。
114
+
115
+ ```bash
116
+ signoff init # 装钩子 + CI + 配置
117
+ signoff init --chain # 已有钩子保留:存为 <name>.local,先跑它
118
+ signoff init --prepare # 同时写进 package.json,队友 npm install 即装钩子
119
+ signoff status # 这个仓库现在守不守?
120
+ signoff doctor # 为什么没按预期生效(只读)
121
+ signoff doctor --fix # 让它把能证明的修掉:重装缺失钩子、补执行位、清掉没暂存的标记
122
+ signoff guard --full # 手动跑同一套(钩子调的就是它)
123
+ signoff audit --range origin/main..HEAD # 这些提交真过过闸门吗?
124
+ signoff audit --since-install # 只看装钩子之后的提交(安装点不在 HEAD 祖先里会直接拒绝)
125
+ signoff init --reset-install # 历史被改写或换了克隆后,把安装点挪到当前 HEAD
126
+ signoff init --lock-test # 把识别到的测试命令写进配置,换机器不再漂移
127
+ signoff uninstall --all # 卸掉钩子 + CI + 配置
128
+ signoff uninstall --keep-config # 卸掉钩子 + CI + 运行时状态,保留配置与账本
129
+ signoff p4 export --merge ../other/.signoff/audit.log # 多仓库合并成一张表
130
+ signoff p4 export --format json --out ledger.json # 也可以导出 json / csv
131
+ signoff explain p2 # 这一关到底查什么、怎么判的
132
+ signoff secrets # 只扫暂存区的密钥泄露(钩子里自动先跑)
133
+ signoff secrets --all --json # 全仓库扫一遍,一条 JSON 判决
134
+ ```
135
+
136
+ ### 密钥扫描与提速(3.12.0 起)
137
+
138
+ - **P1 之前先扫密钥**:内置 16 条高信号规则(AWS / GitHub / GitLab / OpenAI / Google / Slack / Stripe / npm / SendGrid / 私钥块 / Azure / Discord webhook / 带密码的数据库 URL / 通用凭证)。
139
+ 命中即失败;装了 `gitleaks` 可用 `--engine gitleaks` 换全套规则,没装也不影响(纯 Node,无需二进制)。
140
+ - **值不会原样打印**:只显示 `AKIA…EF` 这种前缀 + 规则名 + 行号,避免把日志变成二次泄露。放行写在 `.signoff/config.json` 的 `secrets.allow`。
141
+ - **未 opt-in 的仓库也扫**:机器层守护走 `--no-state`,扫完不留任何文件。
142
+ - **运行器探测扩了 9 种**:go / cargo / maven / gradle / dotnet / ruby / elixir / php / make(`tests/` 与 `package.json` 仍在最前)。
143
+ - **同一暂存树结果缓存**:重复跑 0 ms;**纯文档提交跳过测试**。两个开关都可关:`SIGNOFF_NO_CACHE=1` / `SIGNOFF_NO_FAST=1`。
144
+
145
+ ### 签名与增量覆盖率(3.13.0 起)
146
+
147
+ - **提交自带签名**:`signoff init --sign` 生成仓库本地 ed25519 密钥,`commit-msg` 在 `Signoff-Pass:` 之外再写
148
+ `Signoff-Key:` + `Signoff-Sig:`(对完整 tree 签名,单行 base64)。没有密钥就只写未签名的 `Signoff-Pass`,不阻塞提交。
149
+ - **可以复核**:`signoff audit --verify` 逐个提交验签,`--require-signed` 把未签名也算失败。身份锚点是
150
+ 仓库里提交的 `.signoff/allowed_signers`(或 `signoff.json` 的 `allowed_signers`):不在名单里只报 `self-signed`,绝不报 `verified`。
151
+ - **增量行覆盖率**:`signoff cov --diff --min 80` 只算这次改动新增的行,读项目已有的 `coverage/lcov.info` /
152
+ `coverage-final.json` / python `coverage json`,不额外装运行器;没有覆盖率文件时报"没测过",不会假装通过。
153
+ - **CI 直接标到行**:`GITHUB_ACTIONS=true` 时,密钥/覆盖率发现输出 `::error file=…,line=…` 注解并写步骤摘要,
154
+ PR 页面直接看到,不用翻日志。
155
+
156
+ ### 可核对的凭证(3.15.0 起)
157
+
158
+ `signoff p4 checkpoint` 给账本签一个检查点,`signoff p4 anchor` 打出一行可以贴到发版说明里的锚点:
159
+
160
+ ```
161
+ signoff1:12:9f2c1ab34d0e:71b0c9d2e4a85f61
162
+ ```
163
+
164
+ 事后 `signoff p4 verify --checkpoint --anchor <那一行>` 能分辨四种情况:账本被截断、内容被改、
165
+ 签名不属于受信 key、锚点与本机不一致。细节与边界(它不证明什么)见 [docs/trust.md](docs/trust.md)。
166
+
167
+ 3.15.2 起还补了三件事:`signoff p4 path` 打印账本与检查点的真实位置(脚本不用猜);
168
+ guard 通过后**自动续签**检查点(同一份账本不重复签,不产生无意义改动);账本读取容忍 BOM
169
+ (Windows 编辑器与 `Set-Content -Encoding utf8` 的常见产物)。
170
+
171
+ ### 能力矩阵与匿名竞品(3.15.2)
172
+
173
+ 同一张表看"谁有什么"。竞品按类型匿名(不点名,避免把对比写成广告),对应关系与星标/下载数据
174
+ 记录在 [`docs/signoff-positioning-3.15.2.md`](../docs/signoff-positioning-3.15.2.md)。
175
+
176
+ | 能力 | 落印 | 竞品A 平台级 | 竞品B 钩子框架 | 竞品C 密钥扫描 | 竞品D 文本规则 | 竞品E 供应链证明 | 竞品F 覆盖率 |
177
+ | --- | --- | --- | --- | --- | --- | --- | --- |
178
+ | 测试真的跑过(预算 + 用例数) | 有 | 有 | 无(只负责触发) | 无 | 无 | 无 | 无 |
179
+ | 增量覆盖率分级 | 有 | 有 | 无 | 无 | 无 | 无 | 有 |
180
+ | 密钥扫描 | 有 | 有 | 无 | 有 | 无 | 无 | 无 |
181
+ | 中文文档空话规则 | 有 | 无 | 无 | 无 | 部分(英文词表) | 无 | 无 |
182
+ | 可验签的判决(trailer + key) | 有 | 部分(服务端签名) | 无 | 无 | 无 | 有(attestation) | 无 |
183
+ | 离线账本 + 截断检测 | 有 | 无 | 无 | 无 | 无 | 部分(需 layout/验证器) | 无 |
184
+ | 可发布锚点(一行贴进发版说明) | 有 | 无 | 无 | 无 | 无 | 无 | 无 |
185
+ | 零配置两步上手 | 有 | 无(要服务端 + 项目配置) | 部分(需已有 Node/Python 项目) | 有 | 有 | 无(要密钥与 layout) | 有 |
186
+ | 托管看板 / PR 状态回写 | 无 | 有 | 无 | 无 | 无 | 无 | 无 |
187
+
188
+ 结论放在表后面而不是前面:**前七行是它和其他工具的分野,最后两行是它的短板**——托管面没有,
189
+ 生态采用量也还没起来(1 star、周下载三位数),所以六维里"生态"给 3 分,不是 5 分。
190
+
191
+ ![落印 3.15.2 执行流程与四层证据](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-4-flow.png)
192
+
193
+ 上图是一次提交的完整路径:入口(git commit / 命令 / CI)→ 三层零接触 → guard 固定顺序跑
194
+ secrets → p5 → p1 → p2 → p4 → cov(opt-in)→ 判决 → 四层证据(marker → trailer → 账本 →
195
+ 检查点 → 锚点)。红一关即停,退出码就是判决。
196
+
197
+ ![落印 3.15.2 与匿名竞品:覆盖广度 vs 生态采用度](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-5-competitors.png)
198
+
199
+ 上图横轴是生态采用度,纵轴是覆盖广度。落印落在左上角——**功能全、采用低**,这是它的真实位置:
200
+ 它不在右上角跟平台级产品比采用,差异在"判决可以带走、可以事后核对"。
201
+
202
+ ### 规则包、预设与编辑器(3.14.0 起)
203
+
204
+ - **P5 规则包**:`--pack zh|en|both`。中文词表是默认值,行为不变;`en` 用词边界匹配
205
+ (`very` 不会命中 `delivery`),`both` 两套一起跑并按行去重。`--rules rules.json` 追加自定义词表
206
+ (`ai_words` / `absolute` / `adverbs` / `claims` / `placeholder`),也可写进 `.signoff/config.json`
207
+ 的 `p5` 或 `signoff.json` 的 `p5`。
208
+ - **场景预设**:`signoff init --preset node|python|go|java`,一次给出预算、文档目录、覆盖率阈值和规则包;
209
+ `test_cmd` 仍交给自动探测,克隆到没有该脚本的机器也不会锁死。
210
+ - **VS Code**:`signoff init --editor vscode` 写 `.vscode/tasks.json`(guard / all / secrets / cov / audit / status)
211
+ 与 problem matcher,失败直接落到 Problems 面板;已存在的任务只合并不覆盖。
212
+
213
+ ## 三、在其它代码过程里能干什么
214
+
215
+ | 场景 | 五关怎么用 |
216
+ | --- | --- |
217
+ | 新功能 | P3 先写卡(目标 / scope / 验收 / 回滚)→ P1 让检查命令化并进钩子 → P2 把验收写成场景断言 → P4 发版取票 → P5 文档只留能被数字支撑的句子 |
218
+ | 修 bug | P1 把复现命令固化(`--run` 可重复)→ P2 加一条“反向断言”(旧写法必须红)→ P3 多人并行时按 scope 分波 → P4 单点回滚 → P5 说明里每个数字能落到事实表 |
219
+ | 重构 | P3 的 scope 锁死可动范围,`collect` 抓越界改动;P2 用零写入 / DOM 恒等这类断言证明“行为没变” |
220
+ | CI 流水线 | `signoff all` 一条命令,退出码直接当 CI 判决;P1 的预算参数防止测试变慢却没人发现 |
221
+ | 多 agent 并行 | P3 `plan` 输出 wave(scope 相交的卡绝不同波),`collect` 抓“改了不该改的文件”和“交了空活”的卡 |
222
+ | 交接与复盘 | P2 的覆盖表 + P4 `export` 出的账本,接手的人先看这两张表,再看代码 |
223
+ | 合规留痕 | P4 哈希链:改过的记录会被 `verify` 判为 `content hash mismatch`,事后补账能被发现 |
224
+
225
+ ## 四、在 DSH 任务里能干什么
226
+
227
+ - **发 npm / 改 profile / 改 cordis 配置**:P4 先 `ask` 取票(单次、10 分钟过期、绑定动作类别),再 `confirm` 落账,账里带回滚点;回滚点必须是真的 git ref,写一句“npm deprecate …”会被 `verify` 拒绝。
228
+ - **插件发版**:P1 跑三套真实 DOM fixture,P2 把结果记进覆盖率表,P5 保证发布说明里没有`绝对`化的收尾句。
229
+ - **长任务 / 跨天任务**:P3 的卡片就是断点续传的凭据——scope、验收、回滚点三样齐了,换会话也能接上。
230
+ - **多分叉并行**:不同分叉各拿一张卡,`plan` 先算出不会撞文件的波次,避免两个 agent 同时改同一处。
231
+ - **收尾**:P5 的六条规则让“完成”这句话可以被机器反驳:无数字的`已验证`、`TODO`、`绝对`化断言、AI 味词表(`赋能` / `闭环` / `抓手`…),命中即失败。
232
+ - **插件回归**:`verify/scenarios/*.json` 是 P2 的输入格式,把 fixture 的 checks 数量直接变成覆盖率数字,不需要另写报表。
233
+
234
+ ## 五、命令速查
235
+
236
+ ```bash
237
+ # 自动守护:装一次,之后不用记
238
+ signoff init
239
+ signoff status
240
+ signoff guard --full
241
+ signoff uninstall --all
242
+
243
+ # P1:跑检查;默认 tests/ 用 pytest,否则 npm test
244
+ signoff p1 --run "pytest -q" --run "npm run lint" --budget 120
245
+ signoff p1 --install-hook # 写入 .git/hooks/pre-commit
246
+
247
+ # P2:覆盖分级 + 阈值
248
+ signoff p2 --strict --min-scenario 4 --min-assertions 60 --md docs/coverage.md
249
+
250
+ # P3:卡片校验 / 波次规划 / 收集核对
251
+ signoff p3 validate --cards tasks
252
+ signoff p3 plan --cards tasks
253
+ signoff p3 collect --changed src/a.py src/b.py
254
+
255
+ # P4:取票 → 消费 → 校验 → 导出
256
+ signoff p4 ask --action publish --target "demo@1.2.0"
257
+ signoff p4 confirm --ticket 1a2b3c4d --decision allow --detail "npm publish" --rollback v1.2.0
258
+ signoff p4 verify
259
+ signoff p4 export --out docs/ledger.md
260
+
261
+ # P5:反空话
262
+ signoff p5 docs README.md
263
+ ```
264
+
265
+ 每关都会把结果写到 `.signoff/pN.json`(P4 的账本在仓库内 `.signoff/audit.log`,不在仓库里时退回 `~/.signoff/audit.log`;可用 `SIGNOFF_HOME` 改位置),便于 CI 存档与二次核对。
266
+
267
+ ## 接入方式
268
+
269
+ - **CLI**(本体):`npm i -g signoff-cli`,任何能起进程的地方都能调,退出码即判决。
270
+ - **技能**:`integrations/skill/SKILL.md`,把“退出码是判决、不许绕过、先 doctor 再 explain”教给 agent。
271
+ - **MCP 服务器**:`npx -y dsh-signoff mcp`(或 `integrations/mcp/server.js`),给 Claude Desktop / DSH / IDE 用,五个工具,`isError` 跟随退出码;官方 MCP Registry 条目为 `io.github.ayuanxx-max/signoff`。
272
+ - **宿主插件**:可行但不推荐(只有要画 UI 时才值得),理由见 [docs/integrations.md](docs/integrations.md)。
273
+
274
+ 推广与可发现性的现状和做法见 [docs/promotion.md](docs/promotion.md)。
275
+
276
+ ## 六、边界(先说清不做什么)
277
+
278
+ - **只读不改**:五关不改你的代码,只在 `.signoff/`(P4 账本也在这里;非仓库目录才用 `~/.signoff/`)写结果。
279
+ - **P1 只看退出码**:不做交互、不猜意图;命令怎么写由你给。
280
+ - **P2 不生成断言**:断言要人写;它只负责数、分级、拦住“没到 scenario 级”的降级。
281
+ - **P4 是本机哈希链**,不是外部公证:能发现事后改账,不能防有人连链一起重写。要更强证据得接外部时间戳。
282
+ - **P5 是启发式**:词表可以按团队改(`--allow <rule>` 放行某条),它拦住的是“明显没数字的断言”,不是全部坏文档。
283
+ - **钩子按仓库各装一次**:`signoff init` 写在本机 `.git/hooks/`,别人 clone 不会自动有;CI 那条才是全员生效。
284
+ - **测试命令是“查出来的”,不是“猜出来的”**:按 `package.json` 的 `test` 脚本 → vitest/jest/mocha 依赖 → `Cargo.toml` → `go.mod` → `Makefile` 的 `test` → pytest → unittest 的顺序识别;`signoff init` / `status` / `doctor` 都会写出用的是哪一条、从哪认出来的。`tests/` 是空目录不算测试,不会让 p1 空跑一场报“没收集到用例”。
285
+ - **原有钩子不会被悄悄顶掉**:`signoff init` 默认把现有钩子备份成 `<name>.signoff-backup`(不串接);加 `--chain` 则存成 `<name>.local`(Python/Node/PowerShell 钩子保留 `.py`/`.js`/`.ps1` 后缀,按后缀选解释器)并在我们的钩子里先执行——它失败就停,`signoff uninstall` 会把它原样装回 `<name>`。
286
+ - **账本可以跨仓库合并**:`signoff p4 export --merge <另一个仓库的 audit.log>` 会逐文件重算哈希链,链断了的那份会被标出来并以 1 退出——合并视图不会替坏链背书。
287
+ - **同一仓库同时只跑一次钩子**:`.signoff/lock` 是单实例锁(3.9.1 起是目录 + `owner.json`,用 `mkdir` 抢锁——`O_EXCL` 在 SMB/NFS 上可能两边都成功),第二个提交会拿到“谁在占用”并退 1;锁的持有进程已死或超过 10 分钟自动作废。`commit-msg` 不参与加锁——它不能把已经接受的提交再挡回去。
288
+ - **`doctor` 只读**:它从不安装、不改写、不删除,只把 `status` / `audit` / `p4 verify` 的结论摊开并给出修法;`ok` 判 0,有 `fail` 判 1,不在仓库里判 2。
289
+ - **安装点对不上就拒绝,不给出偏大的范围**:`audit --since-install` 先确认安装点是 HEAD 的祖先(`merge-base --is-ancestor`),rebase、强推、换克隆之后那条提交不在当前历史里,会退 2 并让你跑 `signoff init --reset-install`——审计结论不可信比审计报错更糟。
290
+ - **装钩子之前的历史不算账**:`signoff init` 把当时的提交记进 `.signoff/installed.json`,`signoff audit --since-install` 从那里起算;普通 `audit` 遇到装钩子之前的缺失提交,会多打一行 `N of M missing commit(s) predate the install point`,不用自己算范围。
291
+ - **钩子可绕过,但会留下证据**:`git commit --no-verify` 不触发钩子,提交里就没有 `Signoff-Pass` 标记,`signoff audit`(CI 自动跑)判红;`SIGNOFF_SKIP=1` 写 `Signoff-Skip` 并记进 `.signoff/skips.log`,`--strict` 下判红。
292
+ - **只需要 Node 18+**:闸门与钩子都是 Node;没有 Python、没有其它解释器也能跑完整五关。
293
+ - **不联网**:除 `git` 与本地解释器外不调用外部服务。
294
+
295
+ ## English
296
+
297
+ ### Package name (since 4.0.0)
298
+
299
+ The package is `signoff-cli`; the former name `dsh-signoff` is deprecated. The command
300
+ stays `signoff`, the repository stays `ayuanxx-max/dsh-signoff`, and nothing about the tool
301
+ is DSH-specific — any git repo and any language work.
302
+
303
+ ### At a glance (English)
304
+
305
+ ![SignOff capabilities and relations](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-1-capability-en.png)
306
+
307
+ Six layers, left to right: **entry** (CLI / git hooks / CI / agents) all go through one `guard.js`; **dispatcher**
308
+ decides which gates run and what counts as pass; **the five gates** each state read → decide → fix; **the state
309
+ machine** shows how the marker and the trailer appear inside a single commit; **artifacts** are the evidence a
310
+ verdict leaves behind; **external dependencies** — only Node ≥18 and git are hard requirements.
311
+ There is no partial pass: exit 0 pass, 1 any gate red, 2 usage or environment error.
312
+
313
+ ![User path: install, use, troubleshoot](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-3-user-en.png)
314
+
315
+ Stages 0 → 5 in order: precheck → install → init → daily commits → when it goes red → upgrade and uninstall.
316
+ Red boxes are must-do; amber boxes are the places people trip on (`--chain` when the repo already has husky
317
+ hooks, `--lock-test` when the test command differs per machine, a corporate npm proxy).
318
+ The lower half covers the three dimensions that change what you see without changing the flow: platform
319
+ differences, environment differences, and three red lines (`--no-verify`, `SIGNOFF_SKIP=1`, loosening
320
+ thresholds to turn red green).
321
+
322
+
323
+ > "Done" is a promise. SignOff makes it walk through five gates before anyone signs it.
324
+
325
+ Five gates, five commands, the exit code is the verdict. Each gate maps to one way agent work goes wrong: tests run from memory, a dev page mistaken for verification, parallel workers colliding on the same file, risky actions nobody recorded, documents padded with empty words.
326
+
327
+ | Gate | Command | What it decides |
328
+ | --- | --- | --- |
329
+ | P1 verify | `signoff p1` | Did the checks actually run, inside the budget, with how many cases |
330
+ | P2 coverage | `signoff p2` | What level the verification reached: scenario / harness / smoke / missing |
331
+ | P3 queue | `signoff p3 validate\|plan\|collect` | Cards complete, waves collision-free, nobody out of scope or empty-handed |
332
+ | P4 ledger | `signoff p4 ask\|confirm\|verify\|export` | Did the risky action hold a ticket, is the rollback point still there |
333
+ | P5 anti-slop | `signoff p5` | Empty words, absolute claims, "verified" with no number |
334
+
335
+ ### Secrets and speed (since 3.12.0)
336
+
337
+ - `signoff secrets` scans the index before P1 runs; the guard calls it first, always.
338
+ Sixteen high-signal rules (AWS, GitHub, GitLab, OpenAI, Google, Slack, Stripe, npm,
339
+ SendGrid, private-key blocks, Azure, Discord webhooks, credentialed DB URLs, generic
340
+ `password = "..."`), plain Node, no binary. `--engine gitleaks` uses gitleaks when installed.
341
+ - Values are masked in output (`AKIA…EF`); allow a rule in `.signoff/config.json` under `secrets.allow`.
342
+ - The machine-wide guard scans secrets even in repos that never opted in (`--no-state`: nothing written).
343
+ - Runner detection now covers go / cargo / maven / gradle / dotnet / ruby / elixir / php / make.
344
+ - Same staged tree = cached verdict (0 ms); a docs-only commit skips the suite. Off switches:
345
+ `SIGNOFF_NO_CACHE=1`, `SIGNOFF_NO_FAST=1`.
346
+
347
+ ### Signed verdicts and change coverage (since 3.13.0)
348
+
349
+ - `signoff init --sign` creates a repo-local ed25519 key; `commit-msg` then adds `Signoff-Key:` +
350
+ `Signoff-Sig:` next to `Signoff-Pass:`. The signature covers the full tree id and is a single
351
+ base64 line, so it survives as a trailer. No key means an unsigned trailer - never a blocked commit.
352
+ - `signoff audit --verify` checks each commit's signature; `--require-signed` makes unsigned commits
353
+ fail. Identity needs an anchor: a committed `.signoff/allowed_signers` (or `allowed_signers` in
354
+ `signoff.json`). A key that is not listed is reported `self-signed`, never `verified`.
355
+ - `signoff cov --diff --min 80` counts only lines this change adds, reading the coverage file the
356
+ project already writes (lcov, istanbul, python coverage json). No coverage file means "not
357
+ measured", not "passed".
358
+ - Under `GITHUB_ACTIONS=true`, findings become `::error file=...,line=...` annotations plus a step
359
+ summary, so a reviewer sees them on the diff.
360
+
361
+ ### Checkpointed evidence (since 3.15.0)
362
+
363
+ `signoff p4 checkpoint` signs the ledger head and Merkle root; `signoff p4 anchor`
364
+ prints one line to publish with the release. `signoff p4 verify --checkpoint --anchor`
365
+ then tells truncation, edits, untrusted keys and anchor drift apart. Scope and limits:
366
+ [docs/trust.md](docs/trust.md).
367
+
368
+ Since 3.15.2: `signoff p4 path` prints where the ledger and checkpoint actually live (no guessing in
369
+ scripts); a passing guard re-signs the checkpoint (same ledger, same statement, no churn); ledger
370
+ reads tolerate a UTF-8 BOM, which Windows editors and `Set-Content -Encoding utf8` leave behind.
371
+
372
+ ### Capabilities and anonymised peers (3.15.2)
373
+
374
+ One table for "who has what". Peers are anonymised by category so the comparison reads as a table,
375
+ not an advert; the mapping plus star/download figures live in
376
+ [`docs/signoff-positioning-3.15.2.md`](../docs/signoff-positioning-3.15.2.md).
377
+
378
+ | Capability | SignOff | Peer A platform | Peer B hooks | Peer C secret scan | Peer D text rules | Peer E supply chain | Peer F coverage |
379
+ | --- | --- | --- | --- | --- | --- | --- | --- |
380
+ | Tests actually ran (budget + case count) | yes | yes | no (triggers only) | no | no | no | no |
381
+ | Changed-line coverage levels | yes | yes | no | no | no | no | yes |
382
+ | Secret scanning | yes | yes | no | yes | no | no | no |
383
+ | Chinese prose rules | yes | no | no | no | partial (English only) | no | no |
384
+ | Signed verdict (trailer + key) | yes | partial (server side) | no | no | no | yes (attestation) | no |
385
+ | Offline ledger + truncation detection | yes | no | no | no | no | partial (needs layout/verifier) | no |
386
+ | Publishable anchor (one line in release notes) | yes | no | no | no | no | no | no |
387
+ | Two-command, zero-config start | yes | no (server + project config) | partial (needs a Node/Python project) | yes | yes | no (keys + layout) | yes |
388
+ | Hosted dashboard / PR status | no | yes | no | no | no | no | no |
389
+
390
+ Read the last two rows as the honest part: **no hosted surface, and adoption is still small**
391
+ (1 star, three-digit weekly downloads) — which is why "ecosystem" scores 3, not 5.
392
+
393
+ ![SignOff 3.15.2 flow and four layers of evidence](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-4-flow-en.png)
394
+
395
+ One commit end to end: entries (git commit / CLI / CI) → three zero-touch layers → guard runs
396
+ secrets → p5 → p1 → p2 → p4 → cov (opt-in) → verdict → four layers of evidence (marker → trailer →
397
+ ledger → checkpoint → anchor). First red stops; the exit code is the verdict.
398
+
399
+ ![SignOff 3.15.2 vs anonymised peers](https://raw.githubusercontent.com/ayuanxx-max/dsh-signoff/master/docs/diagrams/signoff-5-competitors-en.png)
400
+
401
+ Adoption on the x-axis, coverage breadth on the y. SignOff sits top-left: **full-stack, low adoption**.
402
+ It does not compete on adoption with platform products; the difference is that a verdict can leave
403
+ the machine and still be checked later.
404
+
405
+ ### Rule packs, presets and editors (since 3.14.0)
406
+
407
+ - `--pack zh|en|both` for gate 5. Chinese stays the default so existing output is unchanged;
408
+ `en` matches on word boundaries (`very` does not fire on `delivery`); `both` runs the two packs
409
+ and de-duplicates per line. `--rules rules.json` adds your own `ai_words` / `absolute` /
410
+ `adverbs` / `claims` / `placeholder`, and the same object can live in `signoff.json` (`p5`).
411
+ - `signoff init --preset node|python|go|java` sets budget, docs, coverage threshold and rule pack.
412
+ `test_cmd` stays auto-detected so a machine without that script still runs something.
413
+ - `signoff init --editor vscode` writes `.vscode/tasks.json` (guard / all / secrets / cov / audit /
414
+ status) plus problem matchers, so a failure lands in the Problems panel. Existing tasks are
415
+ merged, never replaced.
416
+
417
+ ### Zero-touch
418
+
419
+ One command, then nothing: `npm i -g signoff-cli` writes a machine-wide `core.hooksPath`
420
+ (`~/.signoff/hooks`) and bootstraps the repo it is installed from.
421
+
422
+ | Layer | When | Scope |
423
+ | --- | --- | --- |
424
+ | machine | postinstall of the global install | every repo on this machine, including fresh clones |
425
+ | repo | postinstall inside a repo, or `signoff ensure` | `.git/hooks`, `.signoff/config.json`, CI workflow, ledger |
426
+ | declaration | a committed `signoff.json` (`signoff init --share`) | every clone: the machine layer sees it and builds all five gates, no local step |
427
+ | gates | every commit | opted-in repos run all five; unopted repos run only P1 (tests), and write no state |
428
+
429
+ npm 11 may hold `postinstall` behind its script allowlist (`--allow-scripts=dsh-signoff` to allow it), so the
430
+ first gate command in a repo (`signoff p5 README.md`) installs the five gates as a fallback.
431
+
432
+ Committing `signoff.json` is the repository agreeing to P1-P5: `.signoff/` is gitignored, the declaration is not,
433
+ so a clone builds the rest on its first commit. Fields: `docs/signoff.schema.json`.
434
+
435
+ Exit hatches: `SIGNOFF_NO_BOOTSTRAP=1` (no auto install), `SIGNOFF_NO_MACHINE=1` (skip the light guard),
436
+ `signoff uninstall --global` (remove the machine layer). A repo's own hook always runs first and keeps its verdict.
437
+
438
+ ### Case: what the five gates did in the 0.9.34 bug fix after the DSH 2.0.22-next upgrade
439
+
440
+ | Gate | Action in this fix | Evidence | Verdict |
441
+ | --- | --- | --- | --- |
442
+ | P1 | Ran `precommit.ps1` by hand; gate layer 10 ran pytest | `PASS 3666 ms` (101 cases); layer 10 `3438 ms` | Guard worked, but it only covers the tooling's own Python tests: it could not see the front-end bug, and it did not gate the commit automatically (the plugin directory is its own git repo without the hook) |
443
+ | P2 | Three real-DOM scenarios: `menu`(11) / `arrange`(13) / `perf`(6), and registered the package in the coverage ledger | `coverage.py`: apps 4→5, scenario 5/5, assertions 36→77 | **The main work.** Scenario assertions decided "fixed or not"; the ledger keeps them from being one-off evidence |
444
+ | P3 | Not used: no task cards, no wave plan | no `card.json`, no wave record | **Did not contribute.** The three asks were sequential, which is not a reason to skip the paper trail |
445
+ | P4 | Post-hoc audit caught a fake rollback point I wrote; the publish was then recorded properly | `verify` first failed with `rollback point 'npm deprecate …' is not a git ref`; after repair `{"ok": true, "entries": 4, "problems": []}` | **Effective after the fact, absent before it**: publish, release and profile edits ran before any ticket |
446
+ | P5 | Three new documents through the six slop rules; coverage table regenerated by the tool | slop `{"ok": true, "files": 3, "hits": []}` | Worked, but registering the package in coverage happened after the release, so the assertion counts in the notes were not in the ledger yet |
447
+
448
+ One line: **P2 decides whether it is fixed, P5 decides whether I am overclaiming, P1 only proves nothing else broke, P4 caught my own dirty entry after the fact, P3 never left the bench.**
449
+
450
+ <h2 style="color:#c00000">You do not have to do anything differently — just carry on with your task, and it guards the process.</h2>
451
+
452
+ ### Auto-guard: install once, then it runs itself
453
+
454
+ `signoff init` wires the gates into git and CI, so an ordinary `git commit`, `git push` or pull request is checked first:
455
+
456
+ | Installed | What it does |
457
+ | --- | --- |
458
+ | `.git/hooks/pre-commit` | On a change to the index: docs → P5; detectable test command → P1; `verify/scenarios/` → P2 `--strict`; existing ledger → P4 `verify`. A red gate refuses the commit |
459
+ | `.git/hooks/pre-push` | The same set before a push, `--full`, no cache |
460
+ | `.git/hooks/commit-msg` | Writes `Signoff-Pass: <tree>` (or `Signoff-Skip:`) into the message; `signoff audit` and CI use it to tell a gated commit from a bypass |
461
+ | `.github/workflows/signoff.yml` | Every push / PR runs `npx signoff-cli guard --ci`; not bypassable |
462
+ | `.signoff/config.json` | Test command, budget, doc roots; `signoff status` shows all of it |
463
+ | `.gitignore` | `.signoff/` is appended, so results stay out of the repo |
464
+
465
+ Requires **Node 18+ only**: the gates are pure Node (`lib/p*.js`), so no Python is needed and there is no degraded mode.
466
+
467
+ Three stability measures: the same staged tree is judged once (cache hit ~0 ms, so `git commit --amend` costs nothing); a failure prints only the tail of the failing gate (full log in `.signoff/hook.log`); exceeding the budget (default 180 s) is a red gate, not a silent twenty-minute hook.
468
+
469
+ **A bypass leaves a trace**: `signoff audit --range origin/main..HEAD` checks every commit's marker against its tree. Missing means the gates were skipped (`git commit --no-verify`) and is red; a `SIGNOFF_SKIP=1` commit carries `Signoff-Skip` and turns red under `--strict`. The generated CI already runs this step.
470
+
471
+ **Why a teammate ends up guarded too**: with `dsh-signoff` in `devDependencies` (`npm i -D signoff-cli`), `signoff init --prepare` appends `signoff init --quiet` to `package.json`'s `prepare`, so their `npm install` installs the hooks; without it, CI is the backstop.
472
+
473
+ ### Beyond this fix: other code work
474
+
475
+ - **New feature**: card first (P3: goal / scope / acceptance / rollback), then a failing check wired into the hook (P1), then scenario assertions (P2), then a publish ticket (P4), then prose that every number supports (P5).
476
+ - **Bug fix**: freeze the reproduction as a command (P1 `--run`), add a reverse assertion that must go red on the old code (P2), split parallel work into waves (P3), keep one rollback point (P4), and publish only numbers (P5).
477
+ - **Refactor**: scope bounds what may change and `collect` flags out-of-scope edits (P3); zero-write and DOM-identity assertions prove behaviour did not move (P2).
478
+ - **CI**: `signoff all` is one command whose exit code is the verdict; `--budget` catches a suite that quietly got slower.
479
+ - **Multi-agent**: `plan` emits waves where overlapping scopes never share a wave; `collect` reports both "changed a file nobody owns" and "card shipped nothing".
480
+ - **Handover**: the coverage table (P2) plus the exported ledger (P4) are the first two things a new owner should read.
481
+
482
+ ### In DSH tasks
483
+
484
+ - **npm publish, profile edits, cordis config changes**: P4 `ask` mints a single-use ticket (10 minute TTL, bound to the action category); `confirm` appends a hash-chained entry with a rollback point that must be a real git ref.
485
+ - **Plugin releases**: P1 runs the real-DOM fixtures, P2 records them in the coverage ledger, P5 keeps the release notes free of absolute claims.
486
+ - **Long or multi-day tasks**: a card with scope, acceptance and rollback is the resume token - a new session can pick it up.
487
+ - **Parallel forks**: each fork takes a card; `plan` computes waves that cannot collide.
488
+ - **Closing**: P5's six rules make "done" falsifiable by a machine: a numberless "verified", a `TODO`, an absolute claim, or a banned buzzword fail the run.
489
+ - **Plugin regression**: `verify/scenarios/*.json` is P2's input, so fixture check counts become coverage numbers with no extra reporting.
490
+
491
+ ### Usage
492
+
493
+ ```bash
494
+ # auto-guard: install once
495
+ signoff init
496
+ signoff status
497
+ signoff guard --full
498
+ signoff uninstall --all
499
+
500
+ signoff p1 --run "pytest -q" --budget 120
501
+ signoff p1 --install-hook
502
+ signoff p2 --strict --min-scenario 4 --min-assertions 60 --md docs/coverage.md
503
+ signoff p3 validate --cards tasks
504
+ signoff p3 plan --cards tasks
505
+ signoff p3 collect --changed src/a.py
506
+ signoff p4 ask --action publish --target "demo@1.2.0"
507
+ signoff p4 confirm --ticket 1a2b3c4d --decision allow --rollback v1.2.0
508
+ signoff p4 verify
509
+ signoff p5 docs README.md
510
+ ```
511
+
512
+ ### Boundaries
513
+
514
+ - Read-only with respect to your code: results go to `.signoff/` (the P4 ledger included; `~/.signoff/` only outside a repo).
515
+ - P1 judges exit codes only.
516
+ - P2 counts and grades; it does not write assertions for you.
517
+ - P4 is a local hash chain, not external notarisation: it detects edits after the fact, and cannot stop someone rewriting the whole chain.
518
+ - P5 is heuristic; tune the word list with `--allow <rule>`.
519
+ - Hooks are installed per clone; a fresh clone needs `signoff init` again, while CI covers everyone.
520
+ - Hooks are bypassable, but not silently: `git commit --no-verify` leaves no `Signoff-Pass` marker, so `signoff audit` (run by CI) fails the range; `SIGNOFF_SKIP=1` leaves `Signoff-Skip` and is red under `--strict`.
521
+ - Needs Node 18+ only; no Python and no other interpreter is required.
522
+ - No network calls; only `git` and the local interpreters.
523
+
524
+ ### Integrations
525
+
526
+ - **CLI** (the product): `npm i -g signoff-cli`; anything that can spawn a process can run
527
+ it, and the exit code is the verdict.
528
+ - **Skill**: `integrations/skill/SKILL.md` teaches an agent the three things it gets wrong:
529
+ the exit code is the verdict, never bypass a red gate, and `doctor`/`explain` come before
530
+ touching config.
531
+ - **MCP server**: `integrations/mcp/server.js` for Claude Desktop / DSH / IDEs — five tools,
532
+ `isError` mirrors the exit code.
533
+ - **Host plugin**: possible but not recommended; it only pays off when gate results must be
534
+ rendered in a host UI. Reasoning in [docs/integrations.md](docs/integrations.md).
535
+
536
+ Discovery and promotion notes: [docs/promotion.md](docs/promotion.md).
537
+
538
+ ## License
539
+
540
+ MIT
package/action.yml ADDED
@@ -0,0 +1,24 @@
1
+ name: signoff
2
+ description: Five gates before the commit; a red gate stops it. Prints GitHub annotations.
3
+ author: ayuanxx-max
4
+ branding:
5
+ icon: shield
6
+ color: purple
7
+ inputs:
8
+ args:
9
+ description: arguments passed to signoff
10
+ required: false
11
+ default: all
12
+ version:
13
+ description: signoff-cli version to install
14
+ required: false
15
+ default: latest
16
+ runs:
17
+ using: composite
18
+ steps:
19
+ - name: install
20
+ shell: bash
21
+ run: npm i -g signoff-cli@${{ inputs.version }} --no-fund --no-audit
22
+ - name: signoff
23
+ shell: bash
24
+ run: signoff ${{ inputs.args }}