receipts-cli 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shaheer Shoaib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # receipts
2
+
3
+ **Agents need receipts.**
4
+
5
+ Your AI coding agent just told you it fixed the bug. Did it?
6
+
7
+ `receipts` is a verification layer for AI-written code. It does not make your agent
8
+ faster or more autonomous - the whole industry is already building that. It does the
9
+ opposite: it **re-proves the agent's claim before you trust it.** An agent can type
10
+ "Fixed ✅"; it cannot fake the reported symptom still being there when `receipts`
11
+ re-runs it.
12
+
13
+ > Everyone is shipping gas: faster agents, bigger swarms. This ships brakes.
14
+
15
+ ---
16
+
17
+ ## The problem
18
+
19
+ An agent fixes a bug, runs the tests, sees green, and closes the ticket: "Fixed."
20
+ The tests passed. The code looks right. CI is happy. And the bug is still there -
21
+ because the test exercised the wrong thing, or the fix landed on the wrong surface,
22
+ or the change painted correctly in dev and broke in prod, or it patched the symptom
23
+ and not the cause.
24
+
25
+ Real example this was built from: a "modal is cut off" report was read as a vertical
26
+ clip. A height cap was written, tested, deployed, and "verified" green - while the
27
+ real bug was the modal being too *narrow*. The wrong axis shipped. Only a human
28
+ caught it. Every team using AI to write code is hitting some version of this, daily.
29
+
30
+ The missing referee is simple to state and hard to enforce: **a fix is not done
31
+ because the agent says so. It is done when the reported symptom is observably gone
32
+ on the deployed build.**
33
+
34
+ ## The core move: don't trust, re-verify
35
+
36
+ A "looks fixed" screenshot is not a receipt - an agent can produce one for a bug it
37
+ never fixed. A *receipt* is the symptom's own acceptance test, re-run against the
38
+ real build, coming back clean. `receipts` re-runs it. The agent does not get to
39
+ grade its own homework.
40
+
41
+ ## How it works: the Seven Gates
42
+
43
+ The Seven Gates (`spec/SEVEN-GATES.md`) are the standard a fix must clear. Each one
44
+ exists because skipping it shipped a wrong or unverified "fix" at least once - every
45
+ gate carries the real scar that motivated it.
46
+
47
+ They split into two kinds:
48
+
49
+ | Gate | Job | Where it lives |
50
+ |---|---|---|
51
+ | **G0** reproduce the symptom (it IS your acceptance test) | verify | PR / CI (re-run) |
52
+ | **G1** assert the rendered VALUE, not a placeholder | verify | PR / CI (re-run) |
53
+ | **G3** verify on the build that carries YOUR commit | verify | PR / CI |
54
+ | **G5** drive the flow to its TERMINAL action | verify | PR / CI (re-run) |
55
+ | **G2** pin the EXACT flow / component | target | agent-side |
56
+ | **G4** land on the surface the reporter SEES | target | agent-side |
57
+ | **G6** sweep the changed pattern's parallel TWINS | target | agent-side |
58
+
59
+ The **verify** gates (did you actually prove it works) are enforceable at the one
60
+ chokepoint every team shares regardless of which agent they use: the PR. The
61
+ **target** gates (did you fix the *right* thing) live inside the agent's loop, and
62
+ ship as adapters.
63
+
64
+ ## What's in here
65
+
66
+ - **`spec/`** - the Seven Gates standard. The IP. Each gate + its real scar.
67
+ - **`enforcer/`** - the universal piece: a GitHub Action that fails a "fixed" PR
68
+ unless it carries, and *survives*, the receipt (the changed test must be red on
69
+ base, green on head). Agent-agnostic - works no matter who or what wrote the code.
70
+ - **`plugin/`** - a Claude Code plugin (the agent adapter): teaches your agent to
71
+ produce receipts as it works, so its PRs pass the gate naturally.
72
+ - **`plugin/mcp/trajectory-kb/`** - the memory layer: what was tried on a surface and
73
+ how it turned out, so the gates *learn* and stop the team repeating the same trap.
74
+
75
+ ## Install
76
+
77
+ Two independent paths - use either or both.
78
+
79
+ **Enforce it at the PR (any agent):**
80
+ ```yaml
81
+ # .github/workflows/receipts.yml (full template: enforcer/example-workflow.yml)
82
+ on: pull_request
83
+ jobs:
84
+ receipts:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ with: { fetch-depth: 0 }
89
+ - uses: actions/setup-node@v4 # + your deps install (swap per stack)
90
+ - uses: shaheershoaib/receipts/enforcer@main
91
+ ```
92
+
93
+ **Teach your agent to pass it (Claude Code):**
94
+ ```bash
95
+ claude plugin marketplace add shaheershoaib/receipts
96
+ claude plugin install receipts
97
+ ```
98
+
99
+ **Configure it for your project (any stack, any platform):**
100
+ ```bash
101
+ npx receipts-cli init # detects your stack + deploy target, confirms, writes receipts.config.json
102
+ # not published to npm yet? run it straight from the repo, no install:
103
+ # npx github:shaheershoaib/receipts init
104
+ ```
105
+
106
+ It works across any repo because the gate *logic* ships generic and only the project
107
+ *plumbing* (how to test, where it deploys, what marks a fix-claim) is detected per
108
+ project. See [enforcer/GENERALIZATION.md](enforcer/GENERALIZATION.md) for how,
109
+ [enforcer/INIT.md](enforcer/INIT.md) for what `init` detects vs asks, and
110
+ [receipts.config.example.json](receipts.config.example.json) for the output.
111
+
112
+ ## Status
113
+
114
+ Honest: the *discipline* is battle-tested - it has run a production codebase's bug
115
+ pipeline for months and caught real money-path regressions.
116
+
117
+ Built and working today:
118
+ - the Seven Gates spec (`spec/SEVEN-GATES.md`)
119
+ - the focused `seven-gates` agent skill + two Stop-hook backstops (the Claude Code adapter)
120
+ - the `trajectory-kb` memory MCP
121
+ - `receipts init` - detects stack + deploy target, confirms, writes `receipts.config.json`
122
+ - the **CI enforcer** (`enforcer/`) - the red->green re-verification at the PR, as a GitHub Action
123
+
124
+ Next: `verify.live_drive` for symptoms a test can't express (drive the deployed app),
125
+ and an `examples/` demo of a caught wrong-fix.
126
+
127
+ ## License
128
+
129
+ MIT. (The verification discipline should be free and everywhere.)
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * receipts CLI
5
+ *
6
+ * `receipts init` detects a project's plumbing (how it tests, where it deploys,
7
+ * what marks a fix-claim) AND its loop-skill harnesses (the skills that drive the
8
+ * trajectory-kb and that the Stop hooks watch), confirms with you, writes
9
+ * receipts.config.json, and - if the project has no fix/build loop skill - scaffolds
10
+ * one from the bundled template so a clean install reaches parity with no hand-edits.
11
+ *
12
+ * `receipts doctor` re-detects and reports drift against the current config.
13
+ *
14
+ * Zero dependencies - Node built-ins only - so it runs with `npx receipts` or a
15
+ * bare `node bin/receipts.js` and never needs an install step.
16
+ */
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+ const readline = require("readline");
20
+
21
+ const HELP = `receipts - verification gates for AI-written code
22
+
23
+ Usage:
24
+ receipts init [options] Detect this project, confirm, write receipts.config.json
25
+ (+ scaffold a loop-skill harness if none exists)
26
+ receipts doctor [options] Re-detect and report drift against receipts.config.json
27
+
28
+ Options:
29
+ --dir <path> Target repo (default: current directory)
30
+ --yes, -y Accept detected values, skip prompts (CI / scripted)
31
+ --print Print the config to stdout, do not write a file (init)
32
+ --force Overwrite an existing receipts.config.json (init)
33
+ --no-scaffold Do not scaffold a loop-skill harness even if none is found (init)
34
+ --help, -h Show this help
35
+ `;
36
+
37
+ const readText = (p) => { try { return fs.readFileSync(p, "utf8"); } catch { return null; } };
38
+ const readJson = (p) => { const t = readText(p); if (!t) return null; try { return JSON.parse(t); } catch { return null; } };
39
+ const exists = (p) => { try { fs.accessSync(p); return true; } catch { return false; } };
40
+ const dedupe = (arr) => [...new Set(arr.filter(Boolean))];
41
+
42
+ // Detect the project's plumbing + loop-skill harnesses from on-disk artifacts.
43
+ // Never throws.
44
+ function detect(dir) {
45
+ const at = (f) => path.join(dir, f);
46
+ const has = (f) => exists(at(f));
47
+ const hasExt = (ext) => { try { return fs.readdirSync(dir).some((f) => f.endsWith(ext)); } catch { return false; } };
48
+
49
+ // --- test runner ---
50
+ let stack = null, test_command = null, suite_command = null;
51
+ const pkg = readJson(at("package.json"));
52
+ if (pkg && pkg.scripts && pkg.scripts.test) {
53
+ const runner = has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn" : "npm";
54
+ stack = "node";
55
+ suite_command = `${runner} test`;
56
+ test_command = runner === "npm" ? "npm test -- {test}" : `${runner} test {test}`;
57
+ } else if (has("manage.py")) {
58
+ stack = "django"; suite_command = "python manage.py test"; test_command = "python manage.py test {test}";
59
+ } else if (has("pyproject.toml") || has("pytest.ini") || has("setup.cfg") || has("tox.ini")) {
60
+ stack = "python"; suite_command = "pytest"; test_command = "pytest {test}";
61
+ } else if (has("go.mod")) {
62
+ stack = "go"; suite_command = "go test ./..."; test_command = "go test -run {test} ./...";
63
+ } else if (has("Gemfile")) {
64
+ stack = "ruby"; suite_command = "bundle exec rspec"; test_command = "bundle exec rspec {test}";
65
+ } else if (has("Cargo.toml")) {
66
+ stack = "rust"; suite_command = "cargo test"; test_command = "cargo test {test}";
67
+ } else if (has("pom.xml")) {
68
+ stack = "maven"; suite_command = "mvn test"; test_command = "mvn -Dtest={test} test";
69
+ } else if (has("build.gradle") || has("build.gradle.kts")) {
70
+ stack = "gradle"; suite_command = "gradle test"; test_command = "gradle test --tests {test}";
71
+ } else if (hasExt(".csproj") || hasExt(".sln") || hasExt(".fsproj")) {
72
+ stack = "dotnet"; suite_command = "dotnet test"; test_command = "dotnet test --filter {test}";
73
+ } else if (has("composer.json")) {
74
+ stack = "php"; suite_command = "vendor/bin/phpunit"; test_command = "vendor/bin/phpunit {test}";
75
+ } else if (has("mix.exs")) {
76
+ stack = "elixir"; suite_command = "mix test"; test_command = "mix test {test}";
77
+ } else if (has("Makefile") && /(^|\n)test:/.test(readText(at("Makefile")) || "")) {
78
+ stack = "make"; suite_command = "make test"; test_command = "make test";
79
+ }
80
+
81
+ // --- deploy platform ---
82
+ let platform = "none", sha_source = "none", deploy_host_patterns = [];
83
+ const platforms = [
84
+ ["vercel", () => has("vercel.json") || has(".vercel"), ["*.vercel.app"]],
85
+ ["railway", () => has("railway.json") || has("railway.toml"), ["*.up.railway.app", "*.railway.app"]],
86
+ ["netlify", () => has("netlify.toml"), ["*.netlify.app"]],
87
+ ["fly", () => has("fly.toml"), ["*.fly.dev"]],
88
+ ["render", () => has("render.yaml"), ["*.onrender.com"]],
89
+ ["cloudflare", () => has("wrangler.toml") || has("wrangler.jsonc") || has("wrangler.json"), ["*.workers.dev", "*.pages.dev"]],
90
+ ];
91
+ for (const [name, test, hosts] of platforms) {
92
+ if (test()) { platform = name; deploy_host_patterns = hosts; break; }
93
+ }
94
+ if (platform !== "none") sha_source = "github-deployments";
95
+
96
+ // --- loop-skill harnesses (the skills that drive the trajectory-kb + the hooks
97
+ // watch). Scan .claude/skills/*/SKILL.md; a skill whose name or body reads
98
+ // like a fix/build loop is a candidate. ---
99
+ let loop_skills = [];
100
+ const skillsDir = at(".claude/skills");
101
+ try {
102
+ for (const name of fs.readdirSync(skillsDir)) {
103
+ const sk = path.join(skillsDir, name, "SKILL.md");
104
+ if (!exists(sk)) continue;
105
+ // Scan the NAME + the frontmatter description only - the body has incidental
106
+ // keywords ("fix"/"build") that over-match (an audit skill is not a loop).
107
+ const txt = readText(sk) || "";
108
+ const fm = (txt.match(/^---\s*[\r\n]([\s\S]*?)[\r\n]---/) || ["", ""])[1];
109
+ const desc = ((fm.match(/description:\s*([\s\S]*)/i) || ["", ""])[1] || "").slice(0, 400);
110
+ const nameHay = name.toLowerCase();
111
+ if (/loop|retest|feedback|parity|cycle/.test(nameHay + " " + desc.toLowerCase()) || /fix/.test(nameHay)) {
112
+ loop_skills.push(name);
113
+ }
114
+ }
115
+ } catch { /* no .claude/skills dir */ }
116
+
117
+ const repo_name = (pkg && pkg.name) || path.basename(dir);
118
+ return { stack, test_command, suite_command, platform, sha_source, deploy_host_patterns, loop_skills, repo_name };
119
+ }
120
+
121
+ function buildConfig(d, a) {
122
+ const cfg = {
123
+ version: 1,
124
+ claim: {
125
+ issue_link: "closes #(\\d+)",
126
+ downgrade_tags: ["unverified-reasoned", "speculative", "reverted"],
127
+ },
128
+ build: {
129
+ sha_source: d.sha_source,
130
+ platform: d.platform,
131
+ deploy_host_patterns: dedupe([...(d.deploy_host_patterns || []), ...(a.extra_hosts || [])]),
132
+ environments: a.environments || {},
133
+ verify_against: a.verify_against || (d.platform !== "none" ? "staging" : "none"),
134
+ },
135
+ verify: {
136
+ test_command: a.test_command || d.test_command || "REPLACE_ME: how to run ONE acceptance test (use {test} for the path)",
137
+ suite_command: d.suite_command || null,
138
+ live_drive: null,
139
+ },
140
+ degrade: {
141
+ on_no_receipt: "require-downgrade-tag",
142
+ on_unreachable_build: "sha-bind-only",
143
+ },
144
+ agent: {
145
+ // "seven-gates" (the shipped loop) is always watched; project loops merge in.
146
+ loop_skills: dedupe(["seven-gates", ...(a.loop_skills || d.loop_skills || [])]),
147
+ staging_query_patterns: a.staging_query_patterns || [],
148
+ closeout_fixed_statuses: a.closeout_fixed_statuses || ["Pending Retest", "Verified"],
149
+ repo_name: a.repo_name || d.repo_name,
150
+ },
151
+ };
152
+ // Agent-home (skills + cwd, no tests and no deploy): keep only version/claim/agent;
153
+ // the enforcer config (build/verify) belongs in the code repos.
154
+ if (!(a.test_command || d.test_command) && d.platform === "none") {
155
+ delete cfg.build; delete cfg.verify; delete cfg.degrade;
156
+ delete cfg.agent.repo_name; // no single repo at the agent home; each append names its repo
157
+ }
158
+ return cfg;
159
+ }
160
+
161
+ // Fill the bundled loop-skill template and write it into the project's skills dir.
162
+ function scaffoldHarness(dir, vars) {
163
+ const tmplPath = path.join(__dirname, "..", "plugin", "templates", "loop-skill", "SKILL.md.tmpl");
164
+ let tmpl = readText(tmplPath);
165
+ if (!tmpl) return null;
166
+ for (const [k, v] of Object.entries(vars)) tmpl = tmpl.split(`{{${k}}}`).join(v);
167
+ const outDir = path.join(dir, ".claude", "skills", vars.loop_name);
168
+ try {
169
+ fs.mkdirSync(outDir, { recursive: true });
170
+ const outPath = path.join(outDir, "SKILL.md");
171
+ if (exists(outPath)) return outPath; // don't clobber an existing skill
172
+ fs.writeFileSync(outPath, tmpl);
173
+ return outPath;
174
+ } catch { return null; }
175
+ }
176
+
177
+ const ask = (rl, q, def) =>
178
+ new Promise((res) => rl.question(def ? `${q} [${def}] ` : `${q} `, (x) => res((x || "").trim() || def || "")));
179
+ const list = (s) => (s || "").split(",").map((x) => x.trim()).filter(Boolean);
180
+
181
+ async function init(opts) {
182
+ const dir = path.resolve(opts.dir || process.cwd());
183
+ if (!exists(dir)) { console.error(`No such directory: ${dir}`); process.exit(1); }
184
+ const outPath = path.join(dir, "receipts.config.json");
185
+ if (exists(outPath) && !opts.force && !opts.print) {
186
+ console.error("receipts.config.json already exists. Re-run with --force to overwrite, --print to preview, or `receipts doctor` to check drift.");
187
+ process.exit(1);
188
+ }
189
+
190
+ const d = detect(dir);
191
+ // Agent-home = skills + session cwd with no tests and no deploy (e.g. a skills
192
+ // project separate from the code repos): write an agent-only config (no build/verify).
193
+ const agentHome = !d.test_command && d.platform === "none";
194
+ // Diagnostics go to stderr so --print keeps stdout pure JSON.
195
+ console.error(`receipts init - scanning ${dir}\n`);
196
+ console.error(" detected:");
197
+ console.error(` stack ${d.stack || (agentHome ? "agent-home (skills, no code)" : "unknown")}`);
198
+ console.error(` tests ${d.test_command || (agentHome ? "none here (enforcer config lives in the code repos)" : "NOT DETECTED (you'll set verify.test_command)")}`);
199
+ console.error(` deploy ${d.platform === "none" ? "none" : d.platform}`);
200
+ console.error(` loop skills ${d.loop_skills.length ? d.loop_skills.join(", ") : "none found (seven-gates ships with the plugin)"}`);
201
+ console.error("");
202
+
203
+ const a = {};
204
+ if (!opts.yes) {
205
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
206
+ try {
207
+ if (!d.test_command && !agentHome) a.test_command = await ask(rl, "How do you run ONE test? (use {test} for the path)", "");
208
+ if (d.platform !== "none") {
209
+ const env = await ask(rl, "Which environment should receipts re-verify on?", "staging");
210
+ const url = await ask(rl, `URL of '${env}'? (blank to fill in later)`, "");
211
+ a.verify_against = env;
212
+ if (url) a.environments = { [env]: url };
213
+ }
214
+ // Loop-skill harnesses: which skills the trajectory hooks watch + that drive the kb.
215
+ const loopDef = dedupe(["seven-gates", ...d.loop_skills]).join(", ");
216
+ a.loop_skills = list(await ask(rl, "Which skills are your fix/build loops? (comma-separated)", loopDef));
217
+ // Offer to scaffold one if the project has no loop skill of its own.
218
+ const hasProjectLoop = a.loop_skills.some((s) => s !== "seven-gates");
219
+ if (!hasProjectLoop && !opts["no-scaffold"]) {
220
+ const yn = await ask(rl, `No project loop skill found. Scaffold one (${d.repo_name}-fix-loop) from the template?`, "Y");
221
+ if (/^y(es)?$/i.test(yn)) a._scaffold = true;
222
+ }
223
+ const xh = list(await ask(rl, "Extra deploy/prod hosts beyond detected? (comma-separated, blank to skip)", ""));
224
+ if (xh.length) a.extra_hosts = xh;
225
+ const sq = list(await ask(rl, "By-value query hosts/tools (e.g. a DB proxy host)? (blank to skip)", ""));
226
+ if (sq.length) a.staging_query_patterns = sq;
227
+ const go = await ask(rl, "Write receipts.config.json with the above?", "Y");
228
+ if (!/^y(es)?$/i.test(go)) { console.error("Aborted."); rl.close(); process.exit(1); }
229
+ } finally { rl.close(); }
230
+ } else {
231
+ // --yes: register the shipped loop + any detected project loops; scaffold if none.
232
+ a.loop_skills = dedupe(["seven-gates", ...d.loop_skills]);
233
+ if (!d.loop_skills.length && !opts["no-scaffold"]) a._scaffold = true;
234
+ }
235
+
236
+ // Scaffold the harness (before building config, so we can register its name).
237
+ if (a._scaffold && !opts.print) {
238
+ const loop_name = `${d.repo_name}-fix-loop`;
239
+ const written = scaffoldHarness(dir, {
240
+ loop_name,
241
+ repo_name: d.repo_name,
242
+ test_command: d.test_command || a.test_command || "<your test command>",
243
+ platform: d.platform,
244
+ verify_against_url:
245
+ (a.environments && a.verify_against && a.environments[a.verify_against]) ||
246
+ "your deployed build",
247
+ });
248
+ if (written) {
249
+ a.loop_skills = dedupe([...(a.loop_skills || []), loop_name]);
250
+ console.error(`\nScaffolded loop-skill harness: ${written}`);
251
+ }
252
+ }
253
+
254
+ const json = JSON.stringify(buildConfig(d, a), null, 2) + "\n";
255
+ if (opts.print) { process.stdout.write(json); return; }
256
+ fs.writeFileSync(outPath, json);
257
+ JSON.parse(fs.readFileSync(outPath, "utf8")); // round-trip validate
258
+ console.error(`\nWrote ${outPath}`);
259
+ if (agentHome) {
260
+ console.error("Agent-home config (skills + cwd, no build/verify). The Stop hooks read it for");
261
+ console.error("loop skills / hosts / fixed-statuses. Put it at ~/.claude/receipts.config.json to");
262
+ console.error("apply across every session, or in the project root. Run init in your CODE repos");
263
+ console.error("too - there it writes the enforcer's verify/build config.");
264
+ } else {
265
+ console.error("Review it, then commit. The Stop hooks read it (loop skills, hosts, fixed-statuses);");
266
+ console.error("the enforcer reads it (test command, sha source). Each fix still carries its own red->green receipt.");
267
+ }
268
+ }
269
+
270
+ function doctor(opts) {
271
+ const dir = path.resolve(opts.dir || process.cwd());
272
+ const cfg = readJson(path.join(dir, "receipts.config.json"));
273
+ if (!cfg) { console.error("No receipts.config.json here - run `receipts init`."); process.exit(1); }
274
+ const d = detect(dir);
275
+ const drift = [];
276
+ if (d.test_command && cfg.verify && cfg.verify.test_command && d.test_command !== cfg.verify.test_command)
277
+ drift.push(`test_command: config "${cfg.verify.test_command}" vs detected "${d.test_command}"`);
278
+ if (!cfg.verify || !cfg.verify.test_command || /REPLACE_ME/.test(cfg.verify.test_command || ""))
279
+ drift.push("verify.test_command is unset/placeholder");
280
+ if (d.platform !== "none" && cfg.build && d.platform !== cfg.build.platform)
281
+ drift.push(`platform: config "${cfg.build.platform}" vs detected "${d.platform}"`);
282
+ const cfgLoops = (cfg.agent && cfg.agent.loop_skills) || [];
283
+ const missing = (d.loop_skills || []).filter((s) => !cfgLoops.includes(s));
284
+ if (missing.length) drift.push(`loop skills on disk but not in config.agent.loop_skills: ${missing.join(", ")}`);
285
+ if (!cfg.agent) drift.push("config has no `agent` block - the Stop hooks will use generic defaults (re-init to bind project loops/hosts)");
286
+
287
+ if (!drift.length) { console.error("receipts doctor: config looks current."); return; }
288
+ console.error("receipts doctor: drift detected:\n - " + drift.join("\n - ") + "\n\nRe-run `receipts init --force` to refresh.");
289
+ process.exit(2);
290
+ }
291
+
292
+ function parseArgs(argv) {
293
+ const o = { _: [] };
294
+ for (let i = 0; i < argv.length; i++) {
295
+ const x = argv[i];
296
+ if (x === "--dir") o.dir = argv[++i];
297
+ else if (x === "--yes" || x === "-y") o.yes = true;
298
+ else if (x === "--print") o.print = true;
299
+ else if (x === "--force") o.force = true;
300
+ else if (x === "--no-scaffold") o["no-scaffold"] = true;
301
+ else if (x === "--help" || x === "-h") o.help = true;
302
+ else o._.push(x);
303
+ }
304
+ return o;
305
+ }
306
+
307
+ async function main() {
308
+ const o = parseArgs(process.argv.slice(2));
309
+ const cmd = o._[0];
310
+ if (o.help || !cmd) { process.stdout.write(HELP); return; }
311
+ if (cmd === "init") return init(o);
312
+ if (cmd === "doctor") return doctor(o);
313
+ console.error(`Unknown command: ${cmd}\n`);
314
+ process.stdout.write(HELP);
315
+ process.exit(1);
316
+ }
317
+
318
+ main().catch((e) => { console.error(e && e.message ? e.message : e); process.exit(1); });
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "receipts-cli",
3
+ "version": "0.1.3",
4
+ "description": "Verification gates for AI-written code - re-prove an agent's fix before you trust it.",
5
+ "bin": {
6
+ "receipts": "bin/receipts.js"
7
+ },
8
+ "scripts": {
9
+ "init": "node bin/receipts.js init"
10
+ },
11
+ "keywords": ["verification", "ai-agents", "code-review", "ci", "gates", "claude-code"],
12
+ "author": "Shaheer Shoaib <shaheershoaib11@gmail.com>",
13
+ "license": "MIT",
14
+ "repository": { "type": "git", "url": "git+https://github.com/shaheershoaib/receipts.git" },
15
+ "files": ["bin", "plugin/templates", "receipts.config.schema.json", "receipts.config.example.json", "README.md", "LICENSE"],
16
+ "engines": {
17
+ "node": ">=18"
18
+ }
19
+ }
@@ -0,0 +1,61 @@
1
+ # plugin (the Claude Code adapter)
2
+
3
+ Teaches a Claude Code agent to *produce receipts* as it works, so its fixes clear the
4
+ Seven Gates before a PR is ever opened. This is the agent-side half of `receipts`
5
+ (the PR-side half is `../enforcer`).
6
+
7
+ ## What it provides
8
+
9
+ - **`skills/seven-gates/`** - the gates as an agent skill: reproduce-first (G0),
10
+ pin the exact flow (G2), verify by value on the deployed build (G1), land on the
11
+ surface the reporter sees (G4), drive to the terminal action (G5), sweep the twins
12
+ (G6), confirm the sha (G3), and write the red->green receipt. Project-agnostic by
13
+ design - a project supplies its own facts via `receipts.config.json`.
14
+ - **`hooks/stop-verification-gate.py`** - the backstop: blocks a "fixed" close-out
15
+ that lacks deployed-build evidence (binding + observation). The local precursor to
16
+ the CI enforcer.
17
+ - **`hooks/stop-trajectory-reminder.py`** - nudges the agent to record what was tried
18
+ on a surface and how it turned out, so the memory grows (and captures failures, not
19
+ just wins).
20
+ - Pairs with the **`../mcp/trajectory-kb`** server (the verification memory).
21
+
22
+ ## Wiring
23
+
24
+ Claude Code AUTO-DISCOVERS the components from the plugin root: `skills/`,
25
+ `hooks/hooks.json` (the two Stop hooks, referenced via `${CLAUDE_PLUGIN_ROOT}`), and
26
+ `.mcp.json` (the `trajectory-kb` server). The manifest does NOT declare these standard
27
+ paths - declaring a path that resolves to an auto-loaded file fails the load with a
28
+ "Duplicate ... detected" error, so `plugin.json` carries metadata only. Installing the
29
+ plugin registers all three - no hand-editing of settings.json and no `claude mcp add`.
30
+ `claude plugin validate` passes (note: it checks manifest SYNTAX, not the load-time
31
+ duplicate-path error, which only surfaces in `claude plugin list`).
32
+
33
+ ## Project-specifics are config-driven (no hand-editing)
34
+
35
+ The hooks ship sensible generic defaults and MERGE config overrides from
36
+ `receipts.config.json` - the agent-home `~/.claude/receipts.config.json` as a base,
37
+ with the nearest project `receipts.config.json` (walked up from the session cwd)
38
+ merged over it. So a clean install + `receipts init` tunes them with no hand-editing,
39
+ and a **split repo** - skills + session cwd separate from the code repos (e.g. a
40
+ central skills project + several code repos) - is supported via the agent-home layer
41
+ (run `receipts init` there to write an agent-only config; the code repos get the
42
+ enforcer's verify/build config). With no config found the hooks fall back to the
43
+ generic defaults, so a zero-config install still works:
44
+
45
+ - `hooks/stop-verification-gate.py` extends, from config: the deployed-host patterns
46
+ (`build.deploy_host_patterns`), the by-value-query patterns
47
+ (`agent.staging_query_patterns`), the fixed-status values
48
+ (`agent.closeout_fixed_statuses`), and the downgrade tags (`claim.downgrade_tags`).
49
+ - `hooks/stop-trajectory-reminder.py` reads which skills are fix/build loops from
50
+ `agent.loop_skills` (the shipped `seven-gates` plus any project loops), so the
51
+ reminder watches the project's actual loops, not just the bundled one.
52
+ - `skills/seven-gates/` stays project-agnostic. For a project with its own loop,
53
+ `receipts init` registers it in `agent.loop_skills`; for a project with none, `init`
54
+ scaffolds one from `templates/loop-skill/SKILL.md.tmpl` (filled with the project's
55
+ facts) so the trajectory-kb is driven out of the box.
56
+
57
+ ## Roadmap
58
+
59
+ - [x] `receipts.config.json` for host / loop-skill / fixed-status overrides - the hooks read it.
60
+ - [x] `receipts init` detects + registers loop skills and scaffolds a harness when none exists; `receipts doctor` reports drift.
61
+ - [ ] Install-test that the hooks + MCP auto-activate from the manifest in a real session.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: {{loop_name}}
3
+ description: >-
4
+ Use when fixing a bug, addressing a tester or issue report, or claiming a change
5
+ is "done" / "fixed" on {{repo_name}}. The project fix/build loop: query past
6
+ trajectories, apply the Seven Gates (reproduce-first, fix the surface the reporter
7
+ sees, drive to the terminal action, carry a red->green receipt), and record the
8
+ outcome. A fix is done when the reported symptom is observably gone on the
9
+ deployed build, not because you say so.
10
+ ---
11
+
12
+ # {{loop_name}}
13
+
14
+ The {{repo_name}} fix/build loop. It rides on the **seven-gates** discipline (the
15
+ full gate detail + scars live there) and adds the two trajectory-memory touchpoints
16
+ plus this project's facts. Generated by `receipts init`; project facts live in
17
+ `receipts.config.json`, never hardcoded here - re-run `receipts init` if they move.
18
+
19
+ ## At the start (before choosing a fix)
20
+ 1. **Query the trajectory memory** for this surface, to inherit prior dead-ends:
21
+ `query_trajectory({ surface: "<component/route>", text: "<symptom keyword>" })`.
22
+ A past `what_failed` is a wrong-surface / wrong-axis trap pre-recorded - the
23
+ cheapest way to not repeat it.
24
+ 2. **Reproduce the reported symptom (G0).** Observe it and record what you saw;
25
+ that observation is the acceptance test your fix must later show GONE.
26
+
27
+ ## While fixing
28
+ 3. Apply the **Seven Gates** (see the `seven-gates` skill): pin the exact flow the
29
+ reporter used (G2), land on the surface they SEE (G4), assert the rendered VALUE
30
+ not a placeholder (G1), drive the flow to its TERMINAL action (G5), and sweep the
31
+ pattern's parallel twins (G6).
32
+ 4. **Carry a receipt:** a red-before / green-after acceptance test in this project's
33
+ own framework, asserting the reporter's symptom (not a proxy).
34
+ - run one test: `{{test_command}}`
35
+
36
+ ## Verify (G3) on the build that carries YOUR commit
37
+ - Confirm the deployed sha matches your push, then observe the symptom GONE on
38
+ `{{verify_against_url}}` ({{platform}}) by value - read the rendered value or run
39
+ a by-value query, not a "looks fixed" glance.
40
+
41
+ ## At close-out (EVERY exit, not just a clean fix)
42
+ 5. **Record the trajectory** with the HONEST outcome:
43
+ `append_trajectory({ repo: "{{repo_name}}", surface, surface_key, symptom,
44
+ root_cause, outcome, what_worked, what_failed, files })`.
45
+ - `outcome` = `fixed` only if reproduced and observably gone on the right build;
46
+ otherwise `unverified-reasoned` / `speculative` / `reverted` (the honesty
47
+ ladder). Failures are the most valuable entries - put the dead-end in
48
+ `what_failed` so the next loop on this surface inherits it.
49
+
50
+ ## The honesty ladder
51
+ - **fixed** - reproduced and observably gone on the right build (the only success).
52
+ - **unverified-reasoned** - real cause + a test on the path, but you could not
53
+ observe it; route to someone who can, not "fixed."
54
+ - **speculative** - no confirmed cause; loudest flag, human sign-off on high-stakes
55
+ surfaces (money / auth / contracts / destructive migrations).
56
+ - **reverted** - you backed the change out (e.g. wrong surface).
57
+
58
+ "I could not verify this" is a respectable, tracked outcome. A false "fixed" is not.
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "./receipts.config.schema.json",
3
+ "version": 1,
4
+ "claim": {
5
+ "issue_link": "closes #(\\d+)",
6
+ "downgrade_tags": ["unverified-reasoned", "speculative", "reverted"]
7
+ },
8
+ "build": {
9
+ "sha_source": "github-deployments",
10
+ "platform": "vercel",
11
+ "deploy_host_patterns": ["*.vercel.app", "myapp.com"],
12
+ "environments": {
13
+ "staging": "https://myapp-staging.vercel.app",
14
+ "production": "https://myapp.com"
15
+ },
16
+ "verify_against": "staging"
17
+ },
18
+ "verify": {
19
+ "test_command": "npm test -- {test}",
20
+ "suite_command": "npm test",
21
+ "live_drive": null
22
+ },
23
+ "degrade": {
24
+ "on_no_receipt": "require-downgrade-tag",
25
+ "on_unreachable_build": "sha-bind-only"
26
+ },
27
+ "agent": {
28
+ "loop_skills": ["seven-gates", "myapp-fix-loop"],
29
+ "staging_query_patterns": ["proxy.rlwy.net"],
30
+ "closeout_fixed_statuses": ["Fixed - Pending Retest", "Verified"],
31
+ "repo_name": "myapp"
32
+ }
33
+ }
@@ -0,0 +1,129 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/shaheershoaib/receipts/receipts.config.schema.json",
4
+ "title": "receipts project config",
5
+ "description": "Project-specific plumbing for the receipts gates. Produced by `receipts init` (detect -> confirm -> write); rarely hand-authored. The gate LOGIC ships generic; this file only tells receipts how to build/test/reach THIS project and what marks a fix-claim.",
6
+ "type": "object",
7
+ "required": ["version"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "version": { "const": 1, "description": "Config schema version." },
12
+ "claim": {
13
+ "type": "object",
14
+ "description": "How a PR claims to fix an issue, and how it honestly downgrades.",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "issue_link": {
18
+ "type": "string",
19
+ "description": "Regex matching how a PR links the issue it fixes.",
20
+ "default": "closes #(\\d+)"
21
+ },
22
+ "downgrade_tags": {
23
+ "type": "array",
24
+ "items": { "type": "string" },
25
+ "description": "Tags that mark a PR as NOT claiming a clean fix (the honesty ladder); these pass the gate but are tracked, not treated as 'fixed'.",
26
+ "default": ["unverified-reasoned", "speculative", "reverted"]
27
+ }
28
+ }
29
+ },
30
+ "build": {
31
+ "type": "object",
32
+ "description": "How receipts binds to the build under test and (optionally) the deployed app.",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "sha_source": {
36
+ "enum": ["github-deployments", "github-status", "ci-artifact", "none"],
37
+ "description": "How to confirm which commit the build/deploy carries (G3). 'none' = no deploy; verify against the built artifact + test run.",
38
+ "default": "github-deployments"
39
+ },
40
+ "platform": {
41
+ "type": "string",
42
+ "description": "Detected deploy platform (informational), e.g. vercel | railway | netlify | fly | render | none."
43
+ },
44
+ "deploy_host_patterns": {
45
+ "type": "array",
46
+ "items": { "type": "string" },
47
+ "description": "Host globs that count as 'pointed at the deployed build' (for optional live-drive)."
48
+ },
49
+ "environments": {
50
+ "type": "object",
51
+ "additionalProperties": { "type": "string", "format": "uri" },
52
+ "description": "Named environment URLs, e.g. {\"staging\": \"https://...\", \"production\": \"https://...\"}."
53
+ },
54
+ "verify_against": {
55
+ "type": "string",
56
+ "description": "Which environment the enforcer re-verifies on (a key of `environments`).",
57
+ "default": "staging"
58
+ }
59
+ }
60
+ },
61
+ "verify": {
62
+ "type": "object",
63
+ "description": "How to run the receipt: the carried red->green acceptance test. The PRIMARY verification; uses the project's own test framework + CI auth. Required for the ENFORCER (it errors without test_command); an agent-home config (skills + session cwd, no code repo) may omit this whole block - that is why `verify` is not a top-level required field.",
64
+ "required": ["test_command"],
65
+ "additionalProperties": false,
66
+ "properties": {
67
+ "test_command": {
68
+ "type": "string",
69
+ "description": "Run ONE carried acceptance test. `{test}` is substituted with the test path/selector.",
70
+ "examples": ["npm test -- {test}", "pytest {test}", "go test -run {test} ./..."]
71
+ },
72
+ "suite_command": {
73
+ "type": "string",
74
+ "description": "Run the full suite (regression check)."
75
+ },
76
+ "live_drive": {
77
+ "type": ["string", "null"],
78
+ "description": "OPTIONAL advanced: a command/script that drives the deployed app for symptoms a unit/e2e test can't cover. null = not used (the carried test is enough).",
79
+ "default": null
80
+ }
81
+ }
82
+ },
83
+ "degrade": {
84
+ "type": "object",
85
+ "description": "Honest behavior when the symptom cannot be re-verified. Never silent-pass; never block-all.",
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "on_no_receipt": {
89
+ "enum": ["require-downgrade-tag", "warn", "block"],
90
+ "description": "What to do when a fix-claim carries no re-runnable acceptance test.",
91
+ "default": "require-downgrade-tag"
92
+ },
93
+ "on_unreachable_build": {
94
+ "enum": ["sha-bind-only", "warn", "block"],
95
+ "description": "What to do when the deployed build can't be reached for re-verification.",
96
+ "default": "sha-bind-only"
97
+ }
98
+ }
99
+ },
100
+ "agent": {
101
+ "type": "object",
102
+ "description": "How the agent-side Stop hooks bind to THIS project: which skills are fix/build loops (watched by the trajectory reminder + carrying the kb touchpoints), what extra by-value-query patterns count, and which tracker statuses mean 'fixed'. The hooks ship generic defaults; these MERGE OVER them (never replace the generics, so a clean install still works). Written by `receipts init`. Deploy-host patterns live in `build.deploy_host_patterns` (shared with the enforcer).",
103
+ "additionalProperties": false,
104
+ "properties": {
105
+ "loop_skills": {
106
+ "type": "array",
107
+ "items": { "type": "string" },
108
+ "description": "Skill names that are fix/build loops: the trajectory-reminder watches these (append-on-exit) and they carry the kb query/append touchpoints. The shipped 'seven-gates' plus any project loop skills.",
109
+ "default": ["seven-gates"]
110
+ },
111
+ "staging_query_patterns": {
112
+ "type": "array",
113
+ "items": { "type": "string" },
114
+ "description": "Extra substrings / MCP-tool names that count as a by-value query against the deployed build (e.g. a DB proxy host, a query tool), on top of the generic defaults."
115
+ },
116
+ "closeout_fixed_statuses": {
117
+ "type": "array",
118
+ "items": { "type": "string" },
119
+ "description": "Tracker status strings that mean 'claimed fixed' (e.g. 'Fixed - Pending Retest', 'Verified', 'Closed'); the verification gate treats a status update to one of these as a fix close-out.",
120
+ "default": ["Pending Retest", "Verified"]
121
+ },
122
+ "repo_name": {
123
+ "type": "string",
124
+ "description": "Default repo tag for trajectory-kb appends (the agent may override per append)."
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }