signoff-cli 0.0.0-stage → 4.0.0
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/.pre-commit-hooks.yaml +27 -0
- package/CHANGELOG.md +336 -0
- package/LICENSE +21 -0
- package/README.md +540 -3
- package/action.yml +24 -0
- package/bin/signoff.js +218 -0
- package/docs/diagrams/README.md +41 -0
- package/docs/diagrams/signoff-1-capability-en.png +0 -0
- package/docs/diagrams/signoff-1-capability.png +0 -0
- package/docs/diagrams/signoff-2-market-en.png +0 -0
- package/docs/diagrams/signoff-2-market.png +0 -0
- package/docs/diagrams/signoff-3-user-en.png +0 -0
- package/docs/diagrams/signoff-3-user.png +0 -0
- package/docs/diagrams/signoff-4-flow-en.png +0 -0
- package/docs/diagrams/signoff-4-flow.png +0 -0
- package/docs/diagrams/signoff-5-competitors-en.png +0 -0
- package/docs/diagrams/signoff-5-competitors.png +0 -0
- package/docs/diagrams/src/signoff-1-capability-en.mmd +81 -0
- package/docs/diagrams/src/signoff-1-capability.mmd +84 -0
- package/docs/diagrams/src/signoff-2-market-en.mmd +72 -0
- package/docs/diagrams/src/signoff-2-market.mmd +72 -0
- package/docs/diagrams/src/signoff-3-user-en.mmd +94 -0
- package/docs/diagrams/src/signoff-3-user.mmd +92 -0
- package/docs/gates.en.md +105 -0
- package/docs/gates.md +88 -0
- package/docs/integrations.md +58 -0
- package/docs/promotion.md +71 -0
- package/docs/signoff.schema.json +108 -0
- package/docs/trust.md +72 -0
- package/gates/guard.py +474 -0
- package/gates/p1_verify.py +111 -0
- package/gates/p2_coverage.py +127 -0
- package/gates/p3_queue.py +186 -0
- package/gates/p4_ledger.py +249 -0
- package/gates/p5_slop.py +109 -0
- package/lib/audit.js +213 -0
- package/lib/bootstrap.js +361 -0
- package/lib/checkpoint.js +184 -0
- package/lib/ci.js +58 -0
- package/lib/core.js +120 -0
- package/lib/cov.js +253 -0
- package/lib/detect.js +169 -0
- package/lib/doctor.js +304 -0
- package/lib/editor.js +97 -0
- package/lib/explain.js +73 -0
- package/lib/guard.js +1229 -0
- package/lib/p1.js +469 -0
- package/lib/p2.js +323 -0
- package/lib/p3.js +633 -0
- package/lib/p4.js +733 -0
- package/lib/p5.js +438 -0
- package/lib/preset.js +66 -0
- package/lib/secrets.js +336 -0
- package/lib/sign.js +196 -0
- package/package.json +55 -6
- package/test/selftest.js +1226 -0
- package/test/selftest.py +190 -0
package/bin/signoff.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// SignOff (落印) CLI — dispatches to the five gates.
|
|
5
|
+
// The gates are pure Node (lib/p1..p5.js). SIGNOFF_ENGINE=python falls back to
|
|
6
|
+
// the reference implementation in gates/*.py, which is used only for parity tests.
|
|
7
|
+
|
|
8
|
+
const { spawnSync } = require("child_process");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
|
|
12
|
+
// npm >= 11 gates postinstall behind an allowlist, so the install cannot be the only
|
|
13
|
+
// way in: the first gate command in a repo finishes the job itself. Read-only commands
|
|
14
|
+
// (doctor/status/explain/audit) and the hook path stay untouched - the hook path is what
|
|
15
|
+
// the machine-wide guard uses, and there a light P1 is the whole point.
|
|
16
|
+
const BOOTSTRAP_CMDS = new Set(["p1", "p2", "p3", "p4", "p5", "all", "guard", "secrets", "cov"]);
|
|
17
|
+
function maybeBootstrap(cmd) {
|
|
18
|
+
if (!BOOTSTRAP_CMDS.has(cmd)) return;
|
|
19
|
+
if (process.env.SIGNOFF_NO_BOOTSTRAP === "1") return;
|
|
20
|
+
try {
|
|
21
|
+
const r = require("../lib/bootstrap").ensureRepo({ quiet: true });
|
|
22
|
+
if (r.installed && !process.argv.includes("--json")) {
|
|
23
|
+
console.error(`signoff: guarded ${require("path").basename(r.root)} automatically (${r.changed.join(", ")})`);
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
/* a convenience, never a verdict */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const GATES = {
|
|
31
|
+
p1: "p1_verify.py",
|
|
32
|
+
p2: "p2_coverage.py",
|
|
33
|
+
p3: "p3_queue.py",
|
|
34
|
+
p4: "p4_ledger.py",
|
|
35
|
+
p5: "p5_slop.py",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const HELP = `SignOff / 落印 — make "done" provable
|
|
39
|
+
|
|
40
|
+
signoff p1 [--run CMD]... [--budget SEC] gate 1 verify before you speak
|
|
41
|
+
signoff p2 [--min-scenario N] [--md FILE] gate 2 coverage has a level, not a vibe
|
|
42
|
+
signoff p3 [--cards DIR] plan|collect ... gate 3 parallel work, no collisions
|
|
43
|
+
signoff p4 ask|confirm|verify|export ... gate 4 risky actions leave a ledger
|
|
44
|
+
signoff p4 export --merge OTHER... merge another repo's ledger into one table
|
|
45
|
+
signoff p4 export --format {md,json,csv} pick the export shape
|
|
46
|
+
signoff p5 [FILES...] gate 5 empty words are a failure
|
|
47
|
+
|
|
48
|
+
signoff p1 --install-hook drop a git pre-commit hook
|
|
49
|
+
signoff all p1, p2, p5, then p4 verify
|
|
50
|
+
signoff doctor [--fix] show what is present; --fix repairs what it can prove
|
|
51
|
+
|
|
52
|
+
signoff init [--no-ci] [--prepare] [--quiet] install hooks + CI so the gates run themselves
|
|
53
|
+
signoff init --share also write signoff.json, so every clone opts in
|
|
54
|
+
signoff init --chain | --lock-test | --reset-install keep existing hooks / pin the
|
|
55
|
+
detected runner / move the install point to HEAD
|
|
56
|
+
signoff ensure [--global] [--json] idempotent install; gates also self-install
|
|
57
|
+
(--global = guard every repo on this machine)
|
|
58
|
+
signoff guard [--full] run the gate set the hooks use (right now)
|
|
59
|
+
signoff status is this repo guarded?
|
|
60
|
+
signoff doctor [--json] [--fix] why is (or isn't) it guarded? --fix repairs it
|
|
61
|
+
signoff secrets [--staged|--all] [--json] scan staged files for leaked credentials
|
|
62
|
+
signoff cov [--diff] [--min 80] [--json] how much of this change the suite executed\n signoff init --preset node|python|go|java start from a curated gate config\n signoff init --editor vscode .vscode/tasks.json + problem matchers
|
|
63
|
+
signoff explain <p1..p5> what a gate checks and how it decides
|
|
64
|
+
signoff mcp serve the gates over stdio (MCP clients)
|
|
65
|
+
signoff audit [--range A..B] [--strict] did the commits that landed carry a trailer?
|
|
66
|
+
signoff audit --since-install only commits after the install point (refuses
|
|
67
|
+
a point that is not an ancestor of HEAD)
|
|
68
|
+
signoff uninstall [--keep-config] [--all] remove hooks + CI workflow
|
|
69
|
+
|
|
70
|
+
Requires Node >= 18 only; no Python. Options common to every gate: --json, -h.
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
function python() {
|
|
74
|
+
return process.env.SIGNOFF_PYTHON || (process.platform === "win32" ? "python" : "python3");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function jsGate(name) {
|
|
78
|
+
const file = path.join(__dirname, "..", "lib", `${name}.js`);
|
|
79
|
+
return fs.existsSync(file) ? file : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function runGate(name, args) {
|
|
83
|
+
if (process.env.SIGNOFF_ENGINE !== "python" && jsGate(name)) {
|
|
84
|
+
try {
|
|
85
|
+
return require(jsGate(name)).run(args) || 0;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error(`signoff: ${name} failed: ${e && e.message}`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const file = path.join(__dirname, "..", "gates", GATES[name]);
|
|
92
|
+
if (!fs.existsSync(file)) {
|
|
93
|
+
console.error(`signoff: missing gate script ${file}`);
|
|
94
|
+
return 127;
|
|
95
|
+
}
|
|
96
|
+
const r = spawnSync(python(), [file, ...args], { stdio: "inherit" });
|
|
97
|
+
if (r.error) {
|
|
98
|
+
console.error(`signoff: cannot start ${python()} (${r.error.message}). Set SIGNOFF_PYTHON.`);
|
|
99
|
+
return 127;
|
|
100
|
+
}
|
|
101
|
+
return r.status === null ? 1 : r.status;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Secrets run first in `all`: an unstaged key is worth more than a style report,
|
|
105
|
+
// and a non-git directory has nothing to scan (exit 2 from secrets is not a failure).
|
|
106
|
+
function runSecrets(args) {
|
|
107
|
+
try {
|
|
108
|
+
const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
109
|
+
cwd: process.cwd(),
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
});
|
|
113
|
+
if (inside.status !== 0) return 0;
|
|
114
|
+
return require("../lib/secrets.js").run(["--json"]) || 0;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error(`signoff: secrets failed: ${e && e.message}`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function runAll(args) {
|
|
122
|
+
const steps = [
|
|
123
|
+
["p1", args],
|
|
124
|
+
["p2", args],
|
|
125
|
+
["p5", []],
|
|
126
|
+
["p4", ["verify"]],
|
|
127
|
+
// Coverage is opt-in on data: no coverage file means "not measured", not "failed".
|
|
128
|
+
["cov", ["--allow-missing", "--diff"]],
|
|
129
|
+
];
|
|
130
|
+
let code = 0;
|
|
131
|
+
console.log("\n== signoff secrets ==");
|
|
132
|
+
const sc = runSecrets(args);
|
|
133
|
+
if (sc !== 0) code = sc;
|
|
134
|
+
for (const [name, a] of steps) {
|
|
135
|
+
console.log(`\n== signoff ${name} ==`);
|
|
136
|
+
const c = runGate(name, a);
|
|
137
|
+
if (c !== 0) code = c;
|
|
138
|
+
}
|
|
139
|
+
return code;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function guard(sub, args) {
|
|
143
|
+
const file = path.join(__dirname, "..", "lib", "guard.js");
|
|
144
|
+
if (fs.existsSync(file)) {
|
|
145
|
+
try {
|
|
146
|
+
return require(file).main([sub, ...args]) || 0;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error(`signoff: guard failed: ${e && e.message}`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const py = path.join(__dirname, "..", "gates", "guard.py");
|
|
153
|
+
const r = spawnSync(python(), [py, sub, ...args], { stdio: "inherit" });
|
|
154
|
+
if (r.error) {
|
|
155
|
+
console.error(`signoff: cannot start ${python()} (${r.error.message}). Set SIGNOFF_PYTHON.`);
|
|
156
|
+
return 127;
|
|
157
|
+
}
|
|
158
|
+
return r.status === null ? 1 : r.status;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
162
|
+
|
|
163
|
+
if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
164
|
+
console.log(HELP);
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
if (cmd === "-v" || cmd === "--version" || cmd === "version") {
|
|
168
|
+
console.log(require("../package.json").version);
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
if (cmd === "doctor") process.exit(require("../lib/doctor").run(rest));
|
|
172
|
+
if (cmd === "ensure") process.exit(require("../lib/bootstrap").run(rest));
|
|
173
|
+
if (cmd === "all") {
|
|
174
|
+
maybeBootstrap("all");
|
|
175
|
+
process.exit(runAll(rest));
|
|
176
|
+
}
|
|
177
|
+
if (cmd === "init") process.exit(guard("init", rest));
|
|
178
|
+
if (cmd === "guard") {
|
|
179
|
+
maybeBootstrap("guard");
|
|
180
|
+
process.exit(guard("guard", rest));
|
|
181
|
+
}
|
|
182
|
+
if (cmd === "hook") process.exit(guard("hook", rest));
|
|
183
|
+
if (cmd === "status") process.exit(guard("status", rest));
|
|
184
|
+
if (cmd === "uninstall") {
|
|
185
|
+
if (rest.includes("--global")) {
|
|
186
|
+
const r = require("../lib/bootstrap").uninstallGlobal();
|
|
187
|
+
console.log(r.removed ? `signoff: machine layer removed (${r.dir})` : "signoff: machine layer was not installed");
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
process.exit(guard("uninstall", rest));
|
|
191
|
+
}
|
|
192
|
+
if (cmd === "audit") {
|
|
193
|
+
try {
|
|
194
|
+
process.exit(require(path.join(__dirname, "..", "lib", "audit.js")).run(rest) || 0);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.error(`signoff: audit failed: ${e && e.message}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (cmd === "explain") process.exit(require("../lib/explain.js").run(rest));
|
|
201
|
+
if (cmd === "cov") process.exit(require("../lib/cov.js").run(rest));
|
|
202
|
+
if (cmd === "secrets") {
|
|
203
|
+
maybeBootstrap("secrets");
|
|
204
|
+
process.exit(require("../lib/secrets.js").run(rest));
|
|
205
|
+
}
|
|
206
|
+
if (cmd === "mcp") {
|
|
207
|
+
// stdio JSON-RPC; the module wires stdin and stays alive until the client closes it.
|
|
208
|
+
require("../integrations/mcp/server.js");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (GATES[cmd]) {
|
|
212
|
+
maybeBootstrap(cmd);
|
|
213
|
+
process.exit(runGate(cmd, rest));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.error(`signoff: unknown command "${cmd}"\n`);
|
|
217
|
+
console.error(HELP);
|
|
218
|
+
process.exit(2);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SignOff 架构与生态图
|
|
2
|
+
|
|
3
|
+
三张图全部由 **Mermaid** 生成(`mermaid-js/mermaid` + `mermaid-cli`),源文件在 `src/`,
|
|
4
|
+
改文案只要改 `.mmd` 再渲染一次——不手画、不拼版、可 diff。
|
|
5
|
+
|
|
6
|
+
| 源文件 | 产物 | 回答的问题 |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| `src/signoff-1-capability.mmd` | `signoff-1-capability.png` | 由哪些部分组成、判定发生在哪一层、什么会留在仓库里 |
|
|
9
|
+
| `src/signoff-2-market.mmd` | `signoff-2-market.png` | 已出现在哪些市场、哪些等审核、哪些物料备好未提交 |
|
|
10
|
+
| `src/signoff-3-user.mmd` | `signoff-3-user.png` | 用户怎么装怎么用,以及平台/环境差异与已知坑 |
|
|
11
|
+
|
|
12
|
+
## 重新生成
|
|
13
|
+
|
|
14
|
+
```powershell
|
|
15
|
+
$py = "$env:USERPROFILE\.openviking\venv\Scripts\python.exe"
|
|
16
|
+
& $py "$env:USERPROFILE\.dsh\tools\diagram.py" doctor # 检查 mmdc + Brave
|
|
17
|
+
& $py "$env:USERPROFILE\.dsh\tools\diagram.py" render src --out . --size 2200 --theme neutral
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
这台机器上的全局规矩(`~/.dsh/AGENTS.md`「框架图」):画图一律走这条链路。
|
|
21
|
+
选 Mermaid 的理由是它是**可代码驱动**的图工具里星标最多的(91 028);
|
|
22
|
+
`excalidraw/excalidraw` 星更多(135 255)但它是画布应用,没有受支持的 headless CLI,结果不可复现。
|
|
23
|
+
|
|
24
|
+
## 图 1 的读法
|
|
25
|
+
|
|
26
|
+
从左到右:谁在调用 → 谁在判定 → 结果落在哪 → 依赖什么。要点:
|
|
27
|
+
|
|
28
|
+
- 四个入口(CLI、git 钩子、CI、MCP/技能)**共用同一个 `guard.js`**,结论不会互相矛盾;
|
|
29
|
+
- 五关并列,任一判红整体退出码就是 1,**没有“部分通过”**;
|
|
30
|
+
- 提交信息里的 `Signoff-Pass: <tree>` 是审计的唯一线索,`--no-verify` 会让它消失(审计里显示 `missing`)。
|
|
31
|
+
|
|
32
|
+
## 图 2 的读法
|
|
33
|
+
|
|
34
|
+
绿色=已上线可被搜索/安装;黄色=已提交等人工审核;灰色=物料已备未提交。
|
|
35
|
+
底部那句话是关键:**Registry 只是索引,不是流量**;真正带来访问的是人工清单与被引用。
|
|
36
|
+
|
|
37
|
+
## 图 3 的读法
|
|
38
|
+
|
|
39
|
+
上半部分是正常路径(安装 → 初始化 → 日常 → 排错 → 退出/边界),下半部分三支是
|
|
40
|
+
平台差异、环境差异、已知坑。排错那一支刻意写成链条:`doctor → doctor --fix → explain <gate> → 改配置`,
|
|
41
|
+
改阈值永远放最后——90% 的“没反应”是钩子没生效,不是阈值。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SignOff capabilities and relations (v3.9.7)
|
|
3
|
+
---
|
|
4
|
+
flowchart LR
|
|
5
|
+
subgraph L1["① Entry · who calls it"]
|
|
6
|
+
direction TB
|
|
7
|
+
A1["CLI<br/>run · p1..p5 · init · status · doctor<br/>audit · explain · mcp · uninstall<br/>--json · --strict · --dry-run · --chain"]
|
|
8
|
+
A2["Git hooks<br/>pre-commit runs the gates<br/>commit-msg checks the trailer<br/>pre-push checks the audit window<br/>existing hooks chained as .local"]
|
|
9
|
+
A3["CI · signoff.yml<br/>same gates, second run<br/>fetch-depth 0 required for audit"]
|
|
10
|
+
A4["Agents<br/>5 MCP tools · one skill<br/>isError mirrors the exit code"]
|
|
11
|
+
A1 ~~~ A2 ~~~ A3 ~~~ A4
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
subgraph L2["② Dispatcher · guard.js decides what runs and what counts as pass"]
|
|
15
|
+
direction TB
|
|
16
|
+
D1["Parse<br/>cwd normalised to repo root<br/>SIGNOFF_* env · piped runs filtered"]
|
|
17
|
+
D2["Lock<br/>.signoff/lock/ dir + owner.json<br/>atomic mkdir; dead pid or >10 min reclaimed<br/>wait cap SIGNOFF_LOCK_WAIT_MS=8s"]
|
|
18
|
+
D3["Select gates<br/>five by default<br/>name p1..p5; a hook runs only its stage"]
|
|
19
|
+
D4["Verdict<br/>0 all pass · 1 any gate red · 2 usage or env<br/>no partial pass; --json emits the result"]
|
|
20
|
+
D1 ~~~ D2 ~~~ D3 ~~~ D4
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subgraph L3["③ Five gates · each answers: read → decide → fix"]
|
|
24
|
+
direction TB
|
|
25
|
+
P1["P1 Tests<br/>read: config.test_cmd, else auto-detect<br/>decide: test exit code ≠ 0<br/>fix: pin the command in config"]
|
|
26
|
+
P2["P2 Coverage<br/>read: scenario files, assertion count<br/>decide: below threshold, or no assertions<br/>fix: add assertions; --strict is stricter"]
|
|
27
|
+
P3["P3 Queue<br/>read: status in tasks/id/card.json<br/>decide: not done, or fields missing<br/>fix: finish the card's acceptance criteria"]
|
|
28
|
+
P4["P4 Ledger<br/>read: audit.log hash chain + rollback commit<br/>decide: chain broken, rollback gone, trailer missing<br/>fix: p4 ask/confirm, or roll back"]
|
|
29
|
+
P5["P5 Anti-slop<br/>read: docs added by this change<br/>decide: placeholders · claims without evidence<br/>fix: add evidence or delete it"]
|
|
30
|
+
P1 ~~~ P2 ~~~ P3 ~~~ P4 ~~~ P5
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph L4["④ State machine · what happens inside one commit"]
|
|
34
|
+
direction TB
|
|
35
|
+
S1["pre-commit<br/>all five pass → marker {tree, mode} written"]
|
|
36
|
+
S2["commit-msg<br/>marker.tree == HEAD tree ?<br/>yes → append the Signoff-Pass trailer"]
|
|
37
|
+
S3["pre-push<br/>every commit in the audit window checked<br/>missing trailer = red"]
|
|
38
|
+
S4["CI<br/>gates + audit again<br/>no git identity means no fake pass"]
|
|
39
|
+
S1 ~~~ S2 ~~~ S3 ~~~ S4
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
subgraph L5["⑤ Artifacts · judgment leaves evidence"]
|
|
43
|
+
direction TB
|
|
44
|
+
F1["passed<br/>tree + mode(pass|skip) + ts<br/>invalidated when tree changes"]
|
|
45
|
+
F2["audit.log<br/>append-only hash chain: prevHash · action · rollback"]
|
|
46
|
+
F3["installed.json<br/>install-point sha / time / version<br/>left edge of the audit window"]
|
|
47
|
+
F4["commit message<br/>Signoff-Pass: tree<br/>--no-verify is the only hole"]
|
|
48
|
+
F5["config.json<br/>thresholds · test command · P5 word list"]
|
|
49
|
+
F1 ~~~ F2 ~~~ F3 ~~~ F4 ~~~ F5
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
subgraph L6["⑥ External dependencies · only two are hard requirements"]
|
|
53
|
+
direction TB
|
|
54
|
+
X1["Node ≥ 18<br/>pure JS · no build step · no native modules"]
|
|
55
|
+
X2["git<br/>hook location · history · ancestor checks"]
|
|
56
|
+
X3["Test runner<br/>whatever the repo uses; falls back when absent"]
|
|
57
|
+
X4["Python · peer tests only<br/>never invoked at runtime"]
|
|
58
|
+
X5["GitHub Actions<br/>optional, --no-ci skips it"]
|
|
59
|
+
X1 ~~~ X2 ~~~ X3 ~~~ X4 ~~~ X5
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
L1 -->|calls| L2
|
|
63
|
+
L2 -->|picks gates per stage| L3
|
|
64
|
+
L3 -->|writes marker / trailer| L4
|
|
65
|
+
L4 -->|persists| L5
|
|
66
|
+
L5 -->|audit comparison| L6
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
classDef entry fill:#eef3fb,stroke:#2f6fdd,stroke-width:1.4px,color:#0f2d5c
|
|
70
|
+
classDef sched fill:#fdf3e7,stroke:#b45309,stroke-width:1.4px,color:#5c2c05
|
|
71
|
+
classDef gate fill:#e8f7f0,stroke:#0e9f6e,stroke-width:1.4px,color:#0b4a33
|
|
72
|
+
classDef state fill:#eef2ff,stroke:#4f46e5,stroke-width:1.4px,color:#312e81
|
|
73
|
+
classDef data fill:#f3ecfd,stroke:#7c3aed,stroke-width:1.4px,color:#3d1e6b
|
|
74
|
+
classDef ext fill:#f4f5f7,stroke:#6b7280,stroke-width:1.2px,color:#374151
|
|
75
|
+
classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
|
|
76
|
+
class A1,A2,A3,A4 entry
|
|
77
|
+
class D1,D2,D3,D4 sched
|
|
78
|
+
class P1,P2,P3,P4,P5 gate
|
|
79
|
+
class S1,S2,S3,S4 state
|
|
80
|
+
class F1,F2,F3,F4,F5 data
|
|
81
|
+
class X1,X2,X3,X4,X5 ext
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SignOff 完整能力与关系(v3.9.7)
|
|
3
|
+
---
|
|
4
|
+
flowchart LR
|
|
5
|
+
subgraph L1["① 入口层 · 谁在调用它"]
|
|
6
|
+
direction TB
|
|
7
|
+
A1["CLI<br/>run · p1..p5 · init · status · doctor<br/>audit · explain · mcp · uninstall<br/>--json · --strict · --dry-run · --chain"]
|
|
8
|
+
A2["Git 钩子<br/>pre-commit 跑闸门<br/>commit-msg 查 trailer<br/>pre-push 查审计窗口<br/>已有钩子链式为 .local"]
|
|
9
|
+
A3["CI · signoff.yml<br/>同一套闸门再跑一遍<br/>fetch-depth 0 才能审计"]
|
|
10
|
+
A4["Agent 面<br/>MCP 五工具 · 技能<br/>isError 跟随退出码"]
|
|
11
|
+
A1 ~~~ A2 ~~~ A3 ~~~ A4
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
subgraph L2["② 调度层 · guard.js 决定跑什么、谁算通过"]
|
|
15
|
+
direction TB
|
|
16
|
+
D1["解析<br/>cwd 归一到仓库根<br/>SIGNOFF_* 环境 · 管道点 filtered"]
|
|
17
|
+
D2["取锁<br/>.signoff/lock/ 目录 + owner.json<br/>mkdir 原子;死进程或超 10 分钟回收<br/>等待上限 SIGNOFF_LOCK_WAIT_MS=8s"]
|
|
18
|
+
D3["选关<br/>默认五关全跑<br/>可点名 p1..p5;钩子只跑本阶段该跑的"]
|
|
19
|
+
D4["定判决<br/>0 全过 / 1 任一关红 / 2 用法或环境错<br/>--json 输出结构化结果"]
|
|
20
|
+
D1 ~~~ D2 ~~~ D3 ~~~ D4
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subgraph L3["③ 五关 · 每关都回答:读什么 → 判什么 → 怎么修"]
|
|
24
|
+
direction TB
|
|
25
|
+
P1["P1 测试<br/>读:config.test_cmd,否则自动识别<br/>判:测试命令退出码 ≠ 0<br/>修:把命令固化进 config"]
|
|
26
|
+
P2["P2 覆盖<br/>读:场景文件数与断言数<br/>判:低于阈值,或声称成功却无断言<br/>修:补断言;--strict 更严"]
|
|
27
|
+
P3["P3 队列<br/>读:tasks/id/card.json 的 status<br/>判:不是 done,或字段缺失<br/>修:把卡片验收条件做完"]
|
|
28
|
+
P4["P4 账本<br/>读:audit.log 哈希链 + 回滚点提交<br/>判:链断、回滚点消失、审计缺 trailer<br/>修:p4 ask/confirm 补记或回滚"]
|
|
29
|
+
P5["P5 反空话<br/>读:本次新增的文档<br/>判:占位符(词边界)· 无证据结论 · 空话词<br/>修:给证据或删句子"]
|
|
30
|
+
P1 ~~~ P2 ~~~ P3 ~~~ P4 ~~~ P5
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph L4["④ 状态机 · 一次提交里发生了什么"]
|
|
34
|
+
direction TB
|
|
35
|
+
S1["pre-commit<br/>五关全过 → 写 marker {tree, mode}"]
|
|
36
|
+
S2["commit-msg<br/>marker.tree == HEAD tree ?<br/>是 → 追加 Signoff-Pass trailer"]
|
|
37
|
+
S3["pre-push<br/>审计窗口内提交逐个查<br/>缺 trailer 即红"]
|
|
38
|
+
S4["CI<br/>再跑五关 + 审计<br/>无 git 身份时不冒充通过"]
|
|
39
|
+
S1 ~~~ S2 ~~~ S3 ~~~ S4
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
subgraph L5["⑤ 产物与记录 · 判断留下证据"]
|
|
43
|
+
direction TB
|
|
44
|
+
F1["passed<br/>tree + mode(pass|skip) + ts<br/>tree 变了自动失效"]
|
|
45
|
+
F2["audit.log<br/>只追加哈希链:prevHash · action · rollback"]
|
|
46
|
+
F3["installed.json<br/>安装点 sha / 时间 / 版本<br/>审计窗口左边界"]
|
|
47
|
+
F4["提交信息<br/>Signoff-Pass: tree<br/>--no-verify 的唯一破绽"]
|
|
48
|
+
F5["config.json<br/>阈值 · 测试命令 · P5 词表"]
|
|
49
|
+
F1 ~~~ F2 ~~~ F3 ~~~ F4 ~~~ F5
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
subgraph L6["⑥ 外部依赖 · 只有两样是硬要求"]
|
|
53
|
+
direction TB
|
|
54
|
+
X1["Node ≥ 18<br/>纯 JS · 无构建 · 无原生模块"]
|
|
55
|
+
X2["git<br/>钩子位置 · 提交历史 · 祖先校验"]
|
|
56
|
+
X3["测试运行器<br/>仓库自己那一套;缺失则退化后备命令"]
|
|
57
|
+
X4["Python · 仅对等测试<br/>运行时永不调用"]
|
|
58
|
+
X5["GitHub Actions<br/>可选,--no-ci 跳过"]
|
|
59
|
+
X1 ~~~ X2 ~~~ X3 ~~~ X4 ~~~ X5
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
L1 -->|调用| L2
|
|
63
|
+
L2 -->|按阶段选关| L3
|
|
64
|
+
L3 -->|写 marker / trailer| L4
|
|
65
|
+
L4 -->|落盘| L5
|
|
66
|
+
L5 -->|审计比对| L6
|
|
67
|
+
|
|
68
|
+
N["退出码即判决:0 通过 · 1 判红 · 2 用法与环境<br/>没有“部分通过”这回事"]
|
|
69
|
+
L2 -.-> N
|
|
70
|
+
|
|
71
|
+
classDef entry fill:#eef3fb,stroke:#2f6fdd,stroke-width:1.4px,color:#0f2d5c
|
|
72
|
+
classDef sched fill:#fdf3e7,stroke:#b45309,stroke-width:1.4px,color:#5c2c05
|
|
73
|
+
classDef gate fill:#e8f7f0,stroke:#0e9f6e,stroke-width:1.4px,color:#0b4a33
|
|
74
|
+
classDef state fill:#eef2ff,stroke:#4f46e5,stroke-width:1.4px,color:#312e81
|
|
75
|
+
classDef data fill:#f3ecfd,stroke:#7c3aed,stroke-width:1.4px,color:#3d1e6b
|
|
76
|
+
classDef ext fill:#f4f5f7,stroke:#6b7280,stroke-width:1.2px,color:#374151
|
|
77
|
+
classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
|
|
78
|
+
class A1,A2,A3,A4 entry
|
|
79
|
+
class D1,D2,D3,D4 sched
|
|
80
|
+
class P1,P2,P3,P4,P5 gate
|
|
81
|
+
class S1,S2,S3,S4 state
|
|
82
|
+
class F1,F2,F3,F4,F5 data
|
|
83
|
+
class X1,X2,X3,X4,X5 ext
|
|
84
|
+
class N note
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SignOff publishing and market distribution (2026-11-07)
|
|
3
|
+
---
|
|
4
|
+
flowchart LR
|
|
5
|
+
subgraph SRC["⓪ Single source of truth · everything else is a projection"]
|
|
6
|
+
direction TB
|
|
7
|
+
R1["GitHub master<br/>source · hook templates · CI · docs"]
|
|
8
|
+
R2["npm tarball dsh-signoff@3.9.7<br/>package.json carries mcpName"]
|
|
9
|
+
R3["server.json + smithery.yaml<br/>raw material for listings"]
|
|
10
|
+
R4["docs/diagrams/src/*.mmd<br/>the diagrams are sources too"]
|
|
11
|
+
R1 ~~~ R2 ~~~ R3 ~~~ R4
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
subgraph PUB["① Publish channels · where content is actually downloadable"]
|
|
15
|
+
direction TB
|
|
16
|
+
C1["npm registry<br/>dsh-signoff · latest 3.9.7<br/>the only install path"]
|
|
17
|
+
C2["GitHub Releases<br/>v3.9.7 bilingual notes + tgz"]
|
|
18
|
+
C3["Official MCP Registry<br/>io.github.ayuanxx-max/signoff@3.9.7<br/>mcpName verified"]
|
|
19
|
+
C4["OpenViking docs<br/>viking://resources/dsh/signoff-*"]
|
|
20
|
+
C1 ~~~ C2 ~~~ C3 ~~~ C4
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subgraph IDX["② Indexes and lists · whether anyone can find you"]
|
|
24
|
+
direction TB
|
|
25
|
+
subgraph IDXP["submitted / happens automatically"]
|
|
26
|
+
direction TB
|
|
27
|
+
I1["awesome-mcp-servers · PR #18813<br/>Developer Tools · human review 1–3 days"]
|
|
28
|
+
I2["Glama · auto-indexes the repo and scores it<br/>the listing carries its badge"]
|
|
29
|
+
I1 ~~~ I2
|
|
30
|
+
end
|
|
31
|
+
subgraph IDXR["not submitted yet · material ready"]
|
|
32
|
+
direction TB
|
|
33
|
+
I3["Smithery · smithery.yaml ready<br/>needs GitHub OAuth on the web"]
|
|
34
|
+
I4["mcp.so · PulseMCP<br/>mostly pull the official registry, usually automatic"]
|
|
35
|
+
I5["ClawHub skills market<br/>skill written, not published"]
|
|
36
|
+
I6["other awesome-* lists<br/>git-hooks / pre-commit / ai-agents"]
|
|
37
|
+
I3 ~~~ I4 ~~~ I5 ~~~ I6
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
subgraph SEE["③ Discovery · where users actually come from"]
|
|
42
|
+
direction TB
|
|
43
|
+
V1["GitHub search and topics<br/>11 topics set"]
|
|
44
|
+
V2["click from a curated list"]
|
|
45
|
+
V3["word of mouth / posts / comparisons"]
|
|
46
|
+
V4["npm search: dsh-signoff"]
|
|
47
|
+
V1 ~~~ V2 ~~~ V3 ~~~ V4
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
N1["Key distinction: registry and lists are indexes, they generate no traffic<br/>traffic comes from curated lists, citations and search hits"]
|
|
51
|
+
N2["Progress: channels 4/4 done<br/>indexes = 1 in review + 1 automatic + 1 submittable + 1 synced + 2 not submitted"]
|
|
52
|
+
|
|
53
|
+
SRC -->|publish| PUB
|
|
54
|
+
PUB -->|register as discoverable| IDX
|
|
55
|
+
IDX -->|someone sees it| SEE
|
|
56
|
+
C3 -.->|downstream sync| I4
|
|
57
|
+
C2 -.->|auto-indexed| I2
|
|
58
|
+
IDX -.-> N1
|
|
59
|
+
IDX -.-> N2
|
|
60
|
+
|
|
61
|
+
classDef src fill:#0f172a,stroke:#0f172a,color:#ffffff
|
|
62
|
+
classDef pub fill:#dcfce7,stroke:#0e9f6e,stroke-width:1.4px,color:#14532d
|
|
63
|
+
classDef pending fill:#fef3c7,stroke:#d97706,stroke-width:1.4px,color:#7c2d12
|
|
64
|
+
classDef todo fill:#f1f5f9,stroke:#94a3b8,stroke-width:1.2px,color:#334155
|
|
65
|
+
classDef see fill:#eef2ff,stroke:#4f46e5,stroke-width:1.3px,color:#312e81
|
|
66
|
+
classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
|
|
67
|
+
class R1,R2,R3,R4 src
|
|
68
|
+
class C1,C2,C3,C4 pub
|
|
69
|
+
class I1,I2 pending
|
|
70
|
+
class I3,I4,I5,I6 todo
|
|
71
|
+
class V1,V2,V3,V4 see
|
|
72
|
+
class N1,N2 note
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SignOff 发布与市场分布(2026-11-07)
|
|
3
|
+
---
|
|
4
|
+
flowchart LR
|
|
5
|
+
subgraph SRC["⓪ 唯一事实源 · 其余都是它的投影"]
|
|
6
|
+
direction TB
|
|
7
|
+
R1["GitHub 仓库 master<br/>源码 · 钩子模板 · CI · 文档"]
|
|
8
|
+
R2["npm tarball dsh-signoff@3.9.4<br/>package.json 带 mcpName"]
|
|
9
|
+
R3["server.json + smithery.yaml<br/>市场清单的原料"]
|
|
10
|
+
R4["docs/diagrams/src/*.mmd<br/>图也是源文件"]
|
|
11
|
+
R1 ~~~ R2 ~~~ R3 ~~~ R4
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
subgraph PUB["① 发布渠道 · 内容在这里真正可下载"]
|
|
15
|
+
direction TB
|
|
16
|
+
C1["npm registry<br/>dsh-signoff · latest 3.9.4<br/>唯一安装入口"]
|
|
17
|
+
C2["GitHub Releases<br/>v3.9.4 中英双语 + tgz"]
|
|
18
|
+
C3["官方 MCP Registry<br/>io.github.ayuanxx-max/signoff@3.9.4<br/>已过 mcpName 校验"]
|
|
19
|
+
C4["OpenViking 文档库<br/>viking://resources/dsh/signoff-*"]
|
|
20
|
+
C1 ~~~ C2 ~~~ C3 ~~~ C4
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subgraph IDX["② 索引与目录 · 决定别人能不能找到你"]
|
|
24
|
+
direction TB
|
|
25
|
+
subgraph IDXP["已提交 / 会自动发生"]
|
|
26
|
+
direction TB
|
|
27
|
+
I1["awesome-mcp-servers · PR #18813<br/>Developer Tools 分类 · 人工审核 1–3 天"]
|
|
28
|
+
I2["Glama · 按仓库自动建索引并出分<br/>列表条目自带它的徽章"]
|
|
29
|
+
I1 ~~~ I2
|
|
30
|
+
end
|
|
31
|
+
subgraph IDXR["还没提交 · 物料已备"]
|
|
32
|
+
direction TB
|
|
33
|
+
I3["Smithery · smithery.yaml 已就绪<br/>需 GitHub OAuth 在网页提交"]
|
|
34
|
+
I4["mcp.so · PulseMCP<br/>多拉官方 registry,通常自动收录"]
|
|
35
|
+
I5["ClawHub 技能市场<br/>技能已写好,未发布"]
|
|
36
|
+
I6["其余 awesome-* 清单<br/>git-hooks / pre-commit / ai-agents"]
|
|
37
|
+
I3 ~~~ I4 ~~~ I5 ~~~ I6
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
subgraph SEE["③ 发现入口 · 用户实际从哪里进来"]
|
|
42
|
+
direction TB
|
|
43
|
+
V1["GitHub 搜索与 topics<br/>11 个 topic 已配"]
|
|
44
|
+
V2["清单页点击"]
|
|
45
|
+
V3["别人转述 / 文章 / 对比表"]
|
|
46
|
+
V4["npm 站内搜索 dsh-signoff"]
|
|
47
|
+
V1 ~~~ V2 ~~~ V3 ~~~ V4
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
N1["关键区分:Registry 与目录是索引,不产生流量<br/>流量来自人工清单、被引用、被搜索命中"]
|
|
51
|
+
N2["进度:发布渠道 4/4 完成<br/>索引 = 1 待审 + 1 自动 + 1 可提交 + 1 同步 + 2 未提交"]
|
|
52
|
+
|
|
53
|
+
SRC -->|发布| PUB
|
|
54
|
+
PUB -->|登记为可发现| IDX
|
|
55
|
+
IDX -->|被人看到| SEE
|
|
56
|
+
C3 -.->|下游同步| I4
|
|
57
|
+
C2 -.->|自动建索引| I2
|
|
58
|
+
IDX -.-> N1
|
|
59
|
+
IDX -.-> N2
|
|
60
|
+
|
|
61
|
+
classDef src fill:#0f172a,stroke:#0f172a,color:#ffffff
|
|
62
|
+
classDef pub fill:#dcfce7,stroke:#0e9f6e,stroke-width:1.4px,color:#14532d
|
|
63
|
+
classDef pending fill:#fef3c7,stroke:#d97706,stroke-width:1.4px,color:#7c2d12
|
|
64
|
+
classDef todo fill:#f1f5f9,stroke:#94a3b8,stroke-width:1.2px,color:#334155
|
|
65
|
+
classDef see fill:#eef2ff,stroke:#4f46e5,stroke-width:1.3px,color:#312e81
|
|
66
|
+
classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
|
|
67
|
+
class R1,R2,R3,R4 src
|
|
68
|
+
class C1,C2,C3,C4 pub
|
|
69
|
+
class I1,I2 pending
|
|
70
|
+
class I3,I4,I5,I6 todo
|
|
71
|
+
class V1,V2,V3,V4 see
|
|
72
|
+
class N1,N2 note
|