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 +21 -0
- package/README.md +129 -0
- package/bin/receipts.js +318 -0
- package/package.json +19 -0
- package/plugin/README.md +61 -0
- package/plugin/templates/loop-skill/SKILL.md.tmpl +58 -0
- package/receipts.config.example.json +33 -0
- package/receipts.config.schema.json +129 -0
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.)
|
package/bin/receipts.js
ADDED
|
@@ -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
|
+
}
|
package/plugin/README.md
ADDED
|
@@ -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
|
+
}
|