create-agent-rig 0.4.0 โ 0.6.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/CHANGELOG.md +316 -11
- package/README.md +58 -19
- package/package.json +2 -1
- package/packages/cli/dist/commands/create.js +8 -3
- package/packages/cli/dist/commands/init.js +85 -34
- package/packages/cli/dist/commands/upgrade.js +112 -30
- package/packages/cli/dist/index.js +81 -19
- package/packages/cli/dist/lib/copy-tree.js +35 -6
- package/packages/cli/dist/lib/init-settings.js +12 -0
- package/packages/cli/dist/lib/install-set.js +6 -8
- package/packages/cli/dist/lib/manifest.js +31 -9
- package/packages/cli/dist/lib/safe-path.js +30 -0
- package/scripts/prepare.mjs +1 -1
- package/templates/agent-os/init/AGENTS.md +199 -0
- package/templates/agent-os/init/CLAUDE.md +69 -9
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +105 -0
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +117 -0
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +41 -3
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +1 -1
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +41 -0
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +4 -0
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +29 -0
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +236 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +958 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +102 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.mjs +78 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +396 -0
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +73 -0
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +40 -1
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +71 -1
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +301 -26
- package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +43 -5
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +25 -11
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +127 -0
- package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +180 -0
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +32 -13
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +175 -7
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +503 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +69 -1
- package/templates/agent-os/universal/.claude/rules/invariants.md +104 -19
- package/templates/agent-os/universal/.claude/rules/workflow.md +55 -5
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1260 -0
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +65 -2
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +351 -0
- package/templates/agent-os/universal/.claude/scripts/git-env.mjs +49 -0
- package/templates/agent-os/universal/.claude/scripts/lib/gate-coverage.mjs +306 -0
- package/templates/agent-os/universal/.claude/scripts/lib/revalidation-points.mjs +28 -0
- package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +490 -0
- package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +462 -0
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +33 -17
- package/templates/agent-os/universal/.claude/scripts/queue/as-of.mjs +51 -0
- package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +149 -0
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +809 -28
- package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +160 -0
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +95 -16
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +543 -15
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +457 -47
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +289 -25
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +197 -0
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +316 -0
- package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +180 -0
- package/templates/agent-os/universal/.claude/scripts/run-journal.mjs +435 -0
- package/templates/agent-os/universal/.claude/scripts/run-state.mjs +539 -0
- package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +15 -8
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +239 -0
- package/templates/agent-os/universal/.claude/scripts/verdict.mjs +198 -0
- package/templates/agent-os/universal/.claude/settings.json +11 -2
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +118 -7
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +667 -38
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +359 -26
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +4 -0
- package/templates/agent-os/universal/.codex/hooks.json +70 -0
- package/templates/agent-os/universal/AGENTS.md +166 -0
- package/templates/agent-os/universal/CLAUDE.md +49 -14
- package/templates/agent-os/universal/PLAN.md +7 -40
- package/templates/agent-os/universal/docs/decisions/closing-a-task.md +98 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +108 -0
- package/templates/agent-os/universal/docs/decisions/fail-open-guards.md +43 -0
- package/templates/agent-os/universal/docs/decisions/gate-coverage.md +83 -0
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +72 -0
- package/templates/agent-os/universal/docs/decisions/run-directory.md +44 -0
- package/templates/agent-os/universal/docs/decisions/spacing-rations-mechanisms.md +125 -0
- package/templates/agent-os/universal/docs/decisions/stop-conditions-in-a-file.md +46 -0
- package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +74 -0
- package/templates/agent-os/universal/journal/README.md +101 -0
- package/templates/agent-os/universal/layers.json +45 -2
- package/templates/hash-history.json +310 -49
- package/templates/release-ledger.json +9 -0
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +34 -1
- package/templates/skeleton/aws-serverless/README.md +91 -9
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +9 -2
- package/templates/skeleton/aws-serverless/gitignore +37 -0
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +73 -13
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +136 -4
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +14 -1
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +301 -0
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +137 -0
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +36 -20
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +15 -12
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +33 -6
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +18 -5
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +11 -0
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +120 -1
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +72 -1
- package/templates/skeleton/node-service/README.md +11 -1
- package/templates/skeleton/node-service/gitignore +34 -0
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +47 -10
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +20 -0
- package/templates/skeleton/node-service/services/api/src/main.ts +2 -9
- package/templates/skeleton/node-service/services/api/src/server.ts +93 -10
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +20 -0
- package/templates/skeleton/node-service/services/api/test/server.test.ts +98 -13
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +28 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The unattended flag โ how a hook learns that a loop is running, and what the
|
|
3
|
+
// current item is allowed to touch (AR-51).
|
|
4
|
+
//
|
|
5
|
+
// node .claude/scripts/unattended-flag.mjs on --item AR-51 --run-dir <dir> --allow <prefix> [<prefix>โฆ]
|
|
6
|
+
// node .claude/scripts/unattended-flag.mjs off
|
|
7
|
+
//
|
|
8
|
+
// It is a FILE, not an environment variable: a `PreToolUse` hook is spawned by
|
|
9
|
+
// the harness with the harness's own environment, never with a variable the
|
|
10
|
+
// session exported โ the generator's `test/template/guard-rulebook.test.ts` โบ
|
|
11
|
+
// "only a flag arms it โ an exported RIG_UNATTENDED=1 with no flag changes
|
|
12
|
+
// nothing" pins that side of it โ and in some harnesses an `export` does not
|
|
13
|
+
// even survive to the next Bash call. The kill switch (`stop-flag.mjs`) is a
|
|
14
|
+
// file for the same reason,
|
|
15
|
+
// and this module copies its shape: machine-level, under BOTH homes, so a
|
|
16
|
+
// worktree sees it and a `$HOME` set from `.claude/settings.json` cannot hide it.
|
|
17
|
+
//
|
|
18
|
+
// The flag is JSON, `{ item, runDir, allow }`. `allow` is the list of
|
|
19
|
+
// repo-relative prefixes the current item may write under even though they are
|
|
20
|
+
// part of the rulebook โ the loop writes it at claim time from the paths the
|
|
21
|
+
// item names, and clears the flag when the run ends. A blanket "no rulebook
|
|
22
|
+
// edits while unattended" would stall on the first queue item that touches
|
|
23
|
+
// `queue/*.mjs`, which is most of this queue; the allow-list is what makes the
|
|
24
|
+
// guard livable, and the guard is what makes the allow-list a decision rather
|
|
25
|
+
// than a default.
|
|
26
|
+
//
|
|
27
|
+
// ๐ด Three answers, and the third is not the first: `{ on: false }` when no flag
|
|
28
|
+
// exists โ an attended session, the guard does nothing; `{ on: true, item,
|
|
29
|
+
// runDir, allow }` when it reads; `{ on: true, unreadable: true, why }` when a
|
|
30
|
+
// file is THERE and this module cannot read it as the shape above. A guard that
|
|
31
|
+
// treated the third as the first would be disarmed by a corrupt flag, which is
|
|
32
|
+
// the fail-open bypass `.claude/rules/invariants.md` names.
|
|
33
|
+
//
|
|
34
|
+
// ๐ด An `allow` entry may not WIDEN the rulebook: one that is a prefix of a
|
|
35
|
+
// rulebook prefix โ `.`, `.claude/`, `.claude/scripts/`, `CLAUDE` โ would let
|
|
36
|
+
// the flag disarm the guard for a whole tree while it reports itself as on, so
|
|
37
|
+
// the writer refuses it and a flag carrying one is unreadable. An entry outside
|
|
38
|
+
// the rulebook (`src/`, `.claude/skills/loop/`) is harmless โ such a path is
|
|
39
|
+
// never judged โ and items name those all the time, so it is kept, not
|
|
40
|
+
// refused โ โบ "an allow entry that widens the rulebook โ a prefix of a rulebook prefix such as `.` โ makes the flag unreadable".
|
|
41
|
+
//
|
|
42
|
+
// Bounded: the file is read up to 64 KiB, `allow` is capped at 64 entries, and
|
|
43
|
+
// both limits are refusals, never silent truncation.
|
|
44
|
+
import { closeSync, existsSync, mkdirSync, openSync, readSync, rmSync, writeFileSync } from 'node:fs';
|
|
45
|
+
import { dirname, join } from 'node:path';
|
|
46
|
+
import { realpathSync } from 'node:fs';
|
|
47
|
+
import { fileURLToPath } from 'node:url';
|
|
48
|
+
import { homesOf } from './stop-flag.mjs';
|
|
49
|
+
|
|
50
|
+
export const FLAG_BASENAME = '__PROJECT_NAME__-loop-UNATTENDED';
|
|
51
|
+
export const MAX_FLAG_BYTES = 64 * 1024;
|
|
52
|
+
export const MAX_ALLOW_ENTRIES = 64;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The files that decide what a session may do โ repo-relative prefixes. Owned
|
|
56
|
+
* here because two things read them: the guard, to judge an edit, and the
|
|
57
|
+
* writer above, to refuse an allow-list that reaches outside them.
|
|
58
|
+
*/
|
|
59
|
+
export const RULEBOOK_PREFIXES = Object.freeze([
|
|
60
|
+
'.claude/hooks/',
|
|
61
|
+
'.claude/settings.json',
|
|
62
|
+
'.claude/queue.json',
|
|
63
|
+
'.claude/scripts/queue/',
|
|
64
|
+
'.claude/scripts/decision-router.mjs',
|
|
65
|
+
'.claude/scripts/detect-missed-gate.mjs',
|
|
66
|
+
'.claude/rules/',
|
|
67
|
+
'CLAUDE.md',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/** Is this repo-relative path part of the rulebook? */
|
|
71
|
+
export const isRulebookPath = (rel) =>
|
|
72
|
+
RULEBOOK_PREFIXES.some((prefix) => rel === prefix || rel.startsWith(prefix));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Does this allow entry widen the rulebook โ is it a proper prefix of a rulebook
|
|
76
|
+
* prefix, so that it would admit the whole prefix and more? `.claude/scripts/`
|
|
77
|
+
* widens (it covers `.claude/scripts/queue/` and its siblings); `src/` does not
|
|
78
|
+
* (it covers nothing the guard judges); `.claude/scripts/queue/` does not (it is
|
|
79
|
+
* exactly a rulebook prefix, the ordinary allow entry).
|
|
80
|
+
*/
|
|
81
|
+
export const isWidening = (entry) =>
|
|
82
|
+
typeof entry !== 'string' ||
|
|
83
|
+
entry === '' ||
|
|
84
|
+
RULEBOOK_PREFIXES.some((prefix) => prefix !== entry && prefix.startsWith(entry));
|
|
85
|
+
|
|
86
|
+
/** Every path that arms unattended mode. The env-derived home is first. */
|
|
87
|
+
export const unattendedFlags = (env = process.env) =>
|
|
88
|
+
homesOf(env).map((home) => join(home, '.claude', FLAG_BASENAME));
|
|
89
|
+
|
|
90
|
+
const readCapped = (path) => {
|
|
91
|
+
const fd = openSync(path, 'r');
|
|
92
|
+
try {
|
|
93
|
+
const buffer = Buffer.alloc(MAX_FLAG_BYTES + 1);
|
|
94
|
+
const bytes = readSync(fd, buffer, 0, buffer.length, 0);
|
|
95
|
+
return { bytes, text: buffer.toString('utf8', 0, Math.min(bytes, MAX_FLAG_BYTES)) };
|
|
96
|
+
} finally {
|
|
97
|
+
closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const unreadable = (path, why) => ({ on: true, unreadable: true, path, why });
|
|
102
|
+
|
|
103
|
+
/** The mode the flag declares โ see the header for the three answers. */
|
|
104
|
+
export const readUnattended = (env = process.env) => {
|
|
105
|
+
const path = unattendedFlags(env).find((candidate) => {
|
|
106
|
+
try {
|
|
107
|
+
return existsSync(candidate);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (!path) return { on: false };
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = readCapped(path);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return unreadable(path, `cannot be read: ${error?.code ?? 'read failed'}`);
|
|
118
|
+
}
|
|
119
|
+
if (raw.bytes > MAX_FLAG_BYTES) return unreadable(path, `larger than ${MAX_FLAG_BYTES} bytes`);
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(raw.text);
|
|
123
|
+
} catch {
|
|
124
|
+
return unreadable(path, 'not valid JSON');
|
|
125
|
+
}
|
|
126
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
127
|
+
return unreadable(path, 'not a JSON object');
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(parsed.allow) || parsed.allow.some((entry) => typeof entry !== 'string')) {
|
|
130
|
+
return unreadable(path, '`allow` is not an array of strings');
|
|
131
|
+
}
|
|
132
|
+
if (parsed.allow.length > MAX_ALLOW_ENTRIES) {
|
|
133
|
+
return unreadable(path, `\`allow\` carries more than ${MAX_ALLOW_ENTRIES} entries`);
|
|
134
|
+
}
|
|
135
|
+
const allow = parsed.allow.map((entry) => entry.trim()).filter(Boolean);
|
|
136
|
+
const wide = allow.find(isWidening);
|
|
137
|
+
if (wide !== undefined) {
|
|
138
|
+
return unreadable(
|
|
139
|
+
path,
|
|
140
|
+
`\`allow\` entry ${JSON.stringify(wide)} widens the rulebook (it is a prefix of a rulebook prefix) โ an allow-list narrows the rulebook, never widens it`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
on: true,
|
|
145
|
+
item: typeof parsed.item === 'string' ? parsed.item : null,
|
|
146
|
+
runDir: typeof parsed.runDir === 'string' ? parsed.runDir : null,
|
|
147
|
+
allow,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Write the flag under the env-derived home. Returns the paths written. */
|
|
152
|
+
export const writeUnattended = ({ item, runDir = null, allow = [] } = {}, env = process.env) => {
|
|
153
|
+
if (typeof item !== 'string' || item.trim() === '') {
|
|
154
|
+
throw new Error('the unattended flag needs an item id โ a run without an item has nothing to allow');
|
|
155
|
+
}
|
|
156
|
+
const list = (Array.isArray(allow) ? allow : []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
157
|
+
if (list.length > MAX_ALLOW_ENTRIES) {
|
|
158
|
+
throw new Error(`the allow-list is capped at ${MAX_ALLOW_ENTRIES} entries`);
|
|
159
|
+
}
|
|
160
|
+
const wide = list.find(isWidening);
|
|
161
|
+
if (wide !== undefined) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`allow entry ${JSON.stringify(wide)} widens the rulebook โ it is a prefix of one of ${RULEBOOK_PREFIXES.join(', ')}; ` +
|
|
164
|
+
'an allow-list narrows the rulebook, never widens it. A directory entry needs its trailing slash ' +
|
|
165
|
+
'(`.claude/hooks/`, not `.claude/hooks`).',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const [path] = unattendedFlags(env);
|
|
169
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
170
|
+
writeFileSync(path, `${JSON.stringify({ item: item.trim(), runDir, allow: list }, null, 2)}\n`);
|
|
171
|
+
return [path];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** Remove every flag that exists. Returns the paths removed. */
|
|
175
|
+
export const clearUnattended = (env = process.env) => {
|
|
176
|
+
const removed = [];
|
|
177
|
+
for (const path of unattendedFlags(env)) {
|
|
178
|
+
try {
|
|
179
|
+
if (existsSync(path)) {
|
|
180
|
+
rmSync(path);
|
|
181
|
+
removed.push(path);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// a home this process cannot write is not this run's flag to remove
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return removed;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const invokedDirectly = () => {
|
|
191
|
+
if (!process.argv[1]) return false;
|
|
192
|
+
const real = (p) => {
|
|
193
|
+
try {
|
|
194
|
+
return realpathSync(p);
|
|
195
|
+
} catch {
|
|
196
|
+
return p;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
return real(fileURLToPath(import.meta.url)) === real(process.argv[1]);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (invokedDirectly()) {
|
|
203
|
+
const [word, ...rest] = process.argv.slice(2);
|
|
204
|
+
const valueOf = (flag) => {
|
|
205
|
+
const index = rest.indexOf(flag);
|
|
206
|
+
return index === -1 ? null : (rest[index + 1] ?? null);
|
|
207
|
+
};
|
|
208
|
+
if (word === 'on') {
|
|
209
|
+
const item = valueOf('--item');
|
|
210
|
+
if (!item || item.startsWith('--')) {
|
|
211
|
+
process.stderr.write('unattended-flag on: --item <id> is required โ the flag names the item whose paths are allowed\n');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const allowIndex = rest.indexOf('--allow');
|
|
215
|
+
const allow =
|
|
216
|
+
allowIndex === -1
|
|
217
|
+
? []
|
|
218
|
+
: rest.slice(allowIndex + 1).filter((entry, i, all) => {
|
|
219
|
+
const stop = all.findIndex((e) => e.startsWith('--'));
|
|
220
|
+
return stop === -1 || i < stop;
|
|
221
|
+
});
|
|
222
|
+
let path;
|
|
223
|
+
try {
|
|
224
|
+
[path] = writeUnattended({ item, runDir: valueOf('--run-dir'), allow });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
process.stderr.write(`unattended-flag on: ${error?.message ?? error}\n`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
process.stdout.write(`${path}\n`);
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
if (word === 'off') {
|
|
233
|
+
const removed = clearUnattended();
|
|
234
|
+
process.stdout.write(removed.length === 0 ? 'no unattended flag was set\n' : `${removed.join('\n')}\n`);
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
process.stderr.write(`unknown word: ${word ?? '(none)'}. This CLI has two: on, off.\n`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The verdict CLI โ what a gate runs before it believes a reviewer.
|
|
4
|
+
*
|
|
5
|
+
* node .claude/scripts/verdict.mjs check <file> [gate] # `-` reads stdin
|
|
6
|
+
* node .claude/scripts/verdict.mjs coverage <commit> # reads the run journal
|
|
7
|
+
*
|
|
8
|
+
* `check` reads a gate's report, hands it to `lib/verdict.mjs`, and either prints
|
|
9
|
+
* the parsed verdict on stdout (exit 0) or refuses with a diagnosis on stderr
|
|
10
|
+
* (exit 1). All the deciding lives in the module; this file is the call site.
|
|
11
|
+
*
|
|
12
|
+
* `coverage` answers the question one report cannot: did every reviewer this
|
|
13
|
+
* round asked for actually answer, for the commit being merged? It compares the
|
|
14
|
+
* three sets the run journal already holds โ routed, launched, answered โ through
|
|
15
|
+
* `lib/gate-coverage.mjs`, and it is a READ. It never launches a reviewer and it
|
|
16
|
+
* never writes.
|
|
17
|
+
*
|
|
18
|
+
* ๐ด **Name the gate you launched.** The module reads the report's LAST block
|
|
19
|
+
* (its limit 3), so a capture holding two reviewers' answers end to end says
|
|
20
|
+
* only what the second one said โ a stop that vanishes behind a later pass.
|
|
21
|
+
* With `[gate]`, a block claiming another gate is refused. The argument is
|
|
22
|
+
* optional because a caller checking one report of a known gate does not need
|
|
23
|
+
* it; a caller that fanned reviewers out does.
|
|
24
|
+
*
|
|
25
|
+
* ๐ด **The exit code says whether the REPORT was usable, never what the verdict
|
|
26
|
+
* was.** A well-formed `HOLD` exits 0 and prints `"verdict": "HOLD"` โ a gate
|
|
27
|
+
* that found something is not a gate that broke, and a caller keying on the exit
|
|
28
|
+
* code would read the two as one. Read the word on stdout.
|
|
29
|
+
*
|
|
30
|
+
* ๐ด **stdout stays empty on a refusal.** A caller redirecting it into a file
|
|
31
|
+
* would otherwise capture a verdict this command has just refused, which is the
|
|
32
|
+
* failure the whole schema exists to prevent, reintroduced by the reader.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
|
|
37
|
+
import { coverageOf } from './lib/gate-coverage.mjs';
|
|
38
|
+
import { isCommitId, parseVerdict, safeForDiagnosis } from './lib/verdict.mjs';
|
|
39
|
+
import { readRun } from './run-journal.mjs';
|
|
40
|
+
|
|
41
|
+
const USAGE =
|
|
42
|
+
'usage: node .claude/scripts/verdict.mjs check <file> [gate] ' +
|
|
43
|
+
'(`-` reads the report from stdin)\n' +
|
|
44
|
+
' node .claude/scripts/verdict.mjs coverage <commit> ' +
|
|
45
|
+
'(reads the run journal in $RIG_RUN_DIR)\n';
|
|
46
|
+
|
|
47
|
+
const refuse = (message) => {
|
|
48
|
+
process.stderr.write(message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const readReport = (source) => {
|
|
53
|
+
try {
|
|
54
|
+
return readFileSync(source === '-' ? 0 : source, 'utf8');
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// The path, not a stack: the operator needs to know WHICH report could not
|
|
57
|
+
// be read, and a node trace answers a question nobody asked.
|
|
58
|
+
refuse(
|
|
59
|
+
`verdict: could not read the report at ${source} (${error?.code ?? 'unknown error'}). ` +
|
|
60
|
+
'Nothing was checked.\n',
|
|
61
|
+
);
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const [subcommand, source, expectedGate] = process.argv.slice(2);
|
|
67
|
+
|
|
68
|
+
// Two arms, one branch apart, and telling them apart is the whole value of
|
|
69
|
+
// either: an operator told the subcommand is unknown goes looking for a typo
|
|
70
|
+
// that is not there.
|
|
71
|
+
if (subcommand === undefined) refuse(USAGE);
|
|
72
|
+
|
|
73
|
+
if (subcommand === 'coverage') {
|
|
74
|
+
// The four cases and the fix each one needs, said in the line that names the
|
|
75
|
+
// reviewer โ a single "missing" list makes the reader guess between
|
|
76
|
+
// relaunching a reviewer and going to read why one stayed silent.
|
|
77
|
+
const CASES = [
|
|
78
|
+
['neverLaunched', 'the route asked for it and the fan-out never launched it โ launch it'],
|
|
79
|
+
['unanswered', 'launched, and it did not answer โ no verdict of its own parsed'],
|
|
80
|
+
['unattributed', 'it answered, and its verdict named no commit โ so it cannot say it answered for this one'],
|
|
81
|
+
['stale', 'it answered for another commit โ the head moved after the verdict'],
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const commit = source;
|
|
85
|
+
// The same two arms `check` keeps apart: the subcommand was right and the
|
|
86
|
+
// argument was not supplied. Reporting the opposite sends the operator
|
|
87
|
+
// looking for a typo that is not there.
|
|
88
|
+
if (commit === undefined) {
|
|
89
|
+
refuse(
|
|
90
|
+
'verdict: `coverage` needs the commit the round is about; no commit was given. ' +
|
|
91
|
+
'It is the head the reviewers were launched against โ `git rev-parse HEAD` in the ' +
|
|
92
|
+
`reviewed checkout.\n${USAGE}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The one commit field this command owns, and the only one shaped before it is
|
|
97
|
+
// compared: the journal is the other way in and `recordDecision` takes any
|
|
98
|
+
// non-blank string, which is why `sameCommit` enforces its own floor and
|
|
99
|
+
// ceiling rather than trusting an upstream check. Without this arm
|
|
100
|
+
// `coverage <a-full-sha>garbage` prefix-matched its way to "covered" โ the
|
|
101
|
+
// answer that ends in a merge.
|
|
102
|
+
if (!isCommitId(commit)) {
|
|
103
|
+
refuse(
|
|
104
|
+
`verdict: \`${safeForDiagnosis(commit)}\` is not a commit to ask about. ` +
|
|
105
|
+
'It is 7 to 64 hex characters (0-9a-f), the same shape a verdict may name โ ' +
|
|
106
|
+
'`git rev-parse HEAD` in the reviewed checkout.\n',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const runDir = process.env.RIG_RUN_DIR;
|
|
111
|
+
if (!runDir) {
|
|
112
|
+
// ๐ด Exit 0 with nothing printed is indistinguishable from a clean round,
|
|
113
|
+
// and an unattended session reads it as one. The skip is the honest answer โ
|
|
114
|
+
// this run kept no trace โ and it has to be said out loud.
|
|
115
|
+
process.stdout.write(
|
|
116
|
+
'verdict: coverage skipped โ no run directory is declared (RIG_RUN_DIR is unset), so ' +
|
|
117
|
+
'this run journalled no fan-out and no verdicts. Nothing was checked, which is not ' +
|
|
118
|
+
'the same as nothing being outstanding.\n',
|
|
119
|
+
);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let decisions;
|
|
124
|
+
try {
|
|
125
|
+
({ decisions } = readRun({ runDir }));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
refuse(
|
|
128
|
+
`verdict: the run journal in ${runDir} could not be read, so coverage was not ` +
|
|
129
|
+
`checked (${error?.message ?? 'unknown error'}).\n`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const coverage = coverageOf({ records: decisions, headSha: commit });
|
|
134
|
+
if (coverage.ok) {
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`verdict: coverage complete for ${safeForDiagnosis(commit)} โ ` +
|
|
137
|
+
`${coverage.launched.length} reviewer(s) launched, every one of them answered for ` +
|
|
138
|
+
'that commit.\n',
|
|
139
|
+
);
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const lines = [];
|
|
144
|
+
if (coverage.reason !== undefined) lines.push(` ${coverage.reason}`);
|
|
145
|
+
for (const [key, why] of CASES) {
|
|
146
|
+
// Through the sanitiser like every other quoted value here: the names come
|
|
147
|
+
// from the fan-out record, which `recordDecision` checks as strings and
|
|
148
|
+
// nothing more, and a name carrying a cursor sequence repaints this refusal
|
|
149
|
+
// as a pass for whoever is watching the scrollback.
|
|
150
|
+
for (const reviewer of coverage[key]) lines.push(` ${safeForDiagnosis(reviewer)} โ ${why}`);
|
|
151
|
+
}
|
|
152
|
+
refuse(
|
|
153
|
+
`verdict: the fan-out for ${safeForDiagnosis(commit)} is not covered.\n` +
|
|
154
|
+
`${lines.join('\n')}\n`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (subcommand !== 'check') {
|
|
159
|
+
refuse(`verdict: \`${subcommand}\` is not a subcommand of this tool.\n${USAGE}`);
|
|
160
|
+
}
|
|
161
|
+
if (source === undefined) {
|
|
162
|
+
refuse(`verdict: \`check\` needs the report to read; no file was given.\n${USAGE}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = parseVerdict(readReport(source));
|
|
166
|
+
|
|
167
|
+
if (!result.ok) {
|
|
168
|
+
refuse(
|
|
169
|
+
`verdict: the report at ${source} does not end in a verdict this gate can act on. ` +
|
|
170
|
+
'Treat it as `incomplete` โ the gate did not answer, which is not the same as ' +
|
|
171
|
+
'answering that nothing is wrong.\n' +
|
|
172
|
+
result.problems.map((problem) => ` - ${problem}\n`).join(''),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (expectedGate !== undefined && result.verdict.gate !== expectedGate) {
|
|
177
|
+
refuse(
|
|
178
|
+
`verdict: the report at ${source} was checked as ` +
|
|
179
|
+
`\`${safeForDiagnosis(expectedGate)}\`, and the block it ends with answers for ` +
|
|
180
|
+
`\`${safeForDiagnosis(result.verdict.gate)}\`. One capture holding two gates' ` +
|
|
181
|
+
"answers says only what the second one said, so the first one's verdict โ a stop, " +
|
|
182
|
+
'as often as not โ would go unread. Check each report on its own.\n',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
process.stdout.write(`${JSON.stringify(result.verdict, null, 2)}\n`);
|
|
188
|
+
} catch {
|
|
189
|
+
// A verdict can parse and still be unprintable: nothing refuses an unknown
|
|
190
|
+
// key inside a blocker, and one nested deeply enough defeats the printer.
|
|
191
|
+
// Uncaught it is a raw stack, which this file promises never to emit โ and a
|
|
192
|
+
// legitimate stop then reads as `incomplete` with no way to see why.
|
|
193
|
+
refuse(
|
|
194
|
+
`verdict: the verdict in ${source} parsed, and could not be printed โ a blocker ` +
|
|
195
|
+
'nested far past anything a reader needs. Nothing was printed; flatten the ' +
|
|
196
|
+
'blocker and answer again.\n',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"hooks": {
|
|
3
3
|
"PreToolUse": [
|
|
4
4
|
{
|
|
5
|
-
"matcher": "Write|Edit",
|
|
5
|
+
"matcher": "Write|Edit|MultiEdit|NotebookEdit|apply_patch",
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
{
|
|
12
12
|
"type": "command",
|
|
13
13
|
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-web-boundary.mjs\""
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "command",
|
|
17
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-secret-file.mjs\""
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-rulebook.mjs\""
|
|
14
22
|
}
|
|
15
23
|
]
|
|
16
24
|
},
|
|
@@ -33,7 +41,8 @@
|
|
|
33
41
|
"hooks": [
|
|
34
42
|
{
|
|
35
43
|
"type": "command",
|
|
36
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/gate-stop-dod.mjs\""
|
|
44
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/gate-stop-dod.mjs\"",
|
|
45
|
+
"timeout": 900
|
|
37
46
|
}
|
|
38
47
|
]
|
|
39
48
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: check-premises
|
|
3
|
-
description: Check
|
|
3
|
+
description: Check claims about the code before building on them, or before shipping them. Use after taking a queue item and before the failing test โ whenever the item asserts something exists, is missing, is broken, or works a particular way. Use again before the gate, on the run's own prose, where a behaviour claim nothing backs is UNMEASURED.
|
|
4
4
|
context: fork
|
|
5
5
|
allowed-tools: Read, Grep, Glob, Bash
|
|
6
|
-
argument-hint: <the queue item's text>
|
|
6
|
+
argument-hint: <the queue item's text, or the diff and prose to check>
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
A queue item is a **claim about the code**, written by someone who was not
|
|
@@ -11,8 +11,33 @@ reading the code at the time. "The retry path swallows the error", "there is no
|
|
|
11
11
|
validation on that field", "the worker never gets the second message" โ each of
|
|
12
12
|
those is a premise, and the work that follows is only worth doing if it is true.
|
|
13
13
|
|
|
14
|
-
This skill checks the premises. It
|
|
15
|
-
|
|
14
|
+
This skill checks the premises. It writes nothing, and it has **two entry points**.
|
|
15
|
+
|
|
16
|
+
| entry point | the claims are | the code is | verdicts |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| after selection, **before the Red step** | the queue item's, about code it did not read | the repository | `PREMISES HOLD` / `PREMISE FALSE` / `UNVERIFIABLE` |
|
|
19
|
+
| after the work, **before the gate** | your own, in the rulebook prose the diff touches โ and in the PR description if one exists yet | your diff | `PREMISES HOLD` / `UNVERIFIABLE` / `UNMEASURED` |
|
|
20
|
+
|
|
21
|
+
๐ด **Why the second one exists.** A claim you wrote about a mechanism you did not run
|
|
22
|
+
is cheap to write and expensive to find: `prose-reviewer` reaches it only after loading
|
|
23
|
+
the whole diff, and the fix is an edit to one sentence. Same machinery, same question โ *is this claim true?* โ pointed
|
|
24
|
+
at the text the run wrote instead of the text it was handed.
|
|
25
|
+
|
|
26
|
+
The rest of this skill is written for the first entry point. The second one runs the
|
|
27
|
+
same four steps with the diff as the code, and ยง4 carries what is different.
|
|
28
|
+
|
|
29
|
+
**What "rulebook prose" means here is not a new list** โ it is the set
|
|
30
|
+
`.claude/rules/workflow.md` already uses for the `prose-reviewer` trigger: a rule
|
|
31
|
+
file, a skill, an agent spec, a decision record, `CLAUDE.md`, the README. Where a
|
|
32
|
+
rulebook file exists twice (a template source and a generated copy), check the
|
|
33
|
+
**source**; the copy is composed from it. A comment in a test or a hook is in scope
|
|
34
|
+
too when it asserts behaviour โ the file it lives in does not change what a claim is.
|
|
35
|
+
|
|
36
|
+
๐ด **`PREMISE FALSE` belongs to the first entry point only.** At the second one the
|
|
37
|
+
claims are your own and the remedy is an edit, so a false one is not an escalation:
|
|
38
|
+
it is `UNMEASURED`'s neighbour โ delete or correct the sentence and carry on. Reading
|
|
39
|
+
it as the escalation `loop` ยง6 defines would send a finished branch back to the queue
|
|
40
|
+
over one sentence.
|
|
16
41
|
|
|
17
42
|
## Why it sits here and not in review
|
|
18
43
|
|
|
@@ -62,6 +87,10 @@ Read the code. Not the tests, not the docs, not another queue item โ those are
|
|
|
62
87
|
claims too. Each verified premise gets a `file:line` citation; a premise you
|
|
63
88
|
believe but cannot cite is not verified, it is remembered.
|
|
64
89
|
|
|
90
|
+
At the **second** entry point this inverts for one case: a test is exactly what backs
|
|
91
|
+
a behaviour claim, so reading it is the point. The rule above is about not letting a
|
|
92
|
+
test's *name* stand in for what the code does; ยง4 says which artifacts count.
|
|
93
|
+
|
|
65
94
|
## 4. The verdict
|
|
66
95
|
|
|
67
96
|
| Verdict | When | What happens next |
|
|
@@ -69,6 +98,50 @@ believe but cannot cite is not verified, it is remembered.
|
|
|
69
98
|
| `PREMISES HOLD` | every load-bearing claim checked out, or there were none | proceed to the Red step |
|
|
70
99
|
| `PREMISE FALSE` | a load-bearing claim is contradicted by the code | **stop and report** |
|
|
71
100
|
| `UNVERIFIABLE` | a load-bearing claim could not be decided from the code | report it as unverifiable, name what would decide it, and proceed only under a **labelled assumption** |
|
|
101
|
+
| `UNMEASURED` | **second entry point only:** a sentence you wrote asserts behaviour, and nothing you can point at backs it | **delete the sentence, or turn it into a pointer to the test that proves it** โ before the gate |
|
|
102
|
+
|
|
103
|
+
๐ด **The edit belongs to the calling session, not to this skill.** It reports; the
|
|
104
|
+
caller performs the exit before the gate. (The rule is the one at the top of this
|
|
105
|
+
file โ it writes nothing โ not a property of its tool grant.)
|
|
106
|
+
|
|
107
|
+
๐ด **`UNMEASURED` has exactly two exits, and "reword it" is not one of them.** A
|
|
108
|
+
behaviour claim is either backed or it is not; softening the wording keeps an
|
|
109
|
+
unbacked claim in a document agents follow literally. So either the sentence goes,
|
|
110
|
+
or it becomes `see guard-invariant.example.test.mjs โบ "blocks the violation, and the
|
|
111
|
+
reason names what to do instead"` โ the test's whole name, in a file this project
|
|
112
|
+
carries, so one grep lands on it. `invariants.md` ("State the limits") states the norm this verdict
|
|
113
|
+
enforces.
|
|
114
|
+
|
|
115
|
+
**Two questions, and they have different answers โ conflating them is how an unbacked
|
|
116
|
+
sentence survives this check.**
|
|
117
|
+
|
|
118
|
+
*Is the claim founded?* A test you can name, a command whose output is in front of
|
|
119
|
+
you, or a citation to code that does the thing. What does not count: the queue item
|
|
120
|
+
said so (the item is a claim too โ that is what the first entry point is for), it was
|
|
121
|
+
true of the previous design, or it is obviously right.
|
|
122
|
+
|
|
123
|
+
*May it stay in the file as written?* Only the two forms `invariants.md` requires โ
|
|
124
|
+
**generated** from what it describes, or a **pointer** to the test. A command's output
|
|
125
|
+
is not one of them: you saw it, the reader cannot, so a sentence resting on it is still
|
|
126
|
+
`UNMEASURED` however sure you are. Point at the test that runs that command, or say
|
|
127
|
+
something the reader can check.
|
|
128
|
+
|
|
129
|
+
โ **A measurement of this project's own history fits none of the three**, and that is
|
|
130
|
+
a real gap rather than an oversight: the run that produced it is not in the repository,
|
|
131
|
+
and a journal entry does not travel with a rulebook that ships. So a figure about past
|
|
132
|
+
runs belongs in the journal and **not** in a file other projects receive โ where it
|
|
133
|
+
would arrive with no backing at all.
|
|
134
|
+
|
|
135
|
+
A sentence is qualitative when it names **no quantity a reader could check** โ not a
|
|
136
|
+
count, not a share, not a duration. "This has happened here before" qualifies; "this
|
|
137
|
+
happened twice last month" does not, and moves to the journal.
|
|
138
|
+
|
|
139
|
+
That is not a third exit from `UNMEASURED`. The two exits apply to **the sentence in
|
|
140
|
+
front of you**: it goes, or it becomes a pointer. Writing a different sentence โ one
|
|
141
|
+
that claims no measurement, and so needs none โ is the first exit followed by an
|
|
142
|
+
unrelated edit, not a softened version of the same claim. `invariants.md` has a
|
|
143
|
+
worked instance: "Both have happened here, in the same file, within one review
|
|
144
|
+
cycle." No figure, no measurement claimed, and it ships.
|
|
72
145
|
|
|
73
146
|
๐ด **On `PREMISE FALSE` the answer is stop and report โ never quietly work around
|
|
74
147
|
the false premise by building something adjacent that seems useful.** Write what
|
|
@@ -81,6 +154,43 @@ branch named after a task that does not exist.
|
|
|
81
154
|
so the assumption travels in the open, in the item and in the PR description,
|
|
82
155
|
where the next reader can see which part of the work rests on it.
|
|
83
156
|
|
|
157
|
+
### The verdict block
|
|
158
|
+
|
|
159
|
+
Report in prose for the caller, then end with **exactly one** fenced `json` block
|
|
160
|
+
of this shape, and nothing after it. The word is spaced in prose and one token
|
|
161
|
+
inside the block; both forms are the contract.
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"gate": "check-premises",
|
|
166
|
+
"verdict": "PREMISE_FALSE",
|
|
167
|
+
"blockers": [
|
|
168
|
+
{
|
|
169
|
+
"file": "packages/core/src/note.ts",
|
|
170
|
+
"line": 17,
|
|
171
|
+
"rule": "the item claims the schema does not reject an empty title",
|
|
172
|
+
"note": "it does, here โ the reported bug is a caller that skips the validator"
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"advisories": [],
|
|
176
|
+
"evidence": ["read the validator and both call sites"],
|
|
177
|
+
"headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- `verdict` is `PREMISES_HOLD`, `PREMISE_FALSE`, `UNVERIFIABLE` or `UNMEASURED`
|
|
182
|
+
โ this skill returns no other word, and none of the reviewers' words.
|
|
183
|
+
- One blocker per premise that did not hold, per claim that could not be decided,
|
|
184
|
+
or per unbacked sentence โ `rule` is the claim itself, `note` is what the code
|
|
185
|
+
actually says, and `file`/`line` is the citation ยง3 requires.
|
|
186
|
+
- `PREMISES_HOLD` carries an empty `blockers` list; the other three name at
|
|
187
|
+
least one. A stop verdict without one is not an answer the caller can act on,
|
|
188
|
+
and the caller is what checks: the `loop` skill runs
|
|
189
|
+
`node .claude/scripts/verdict.mjs check <report> check-premises` on what you
|
|
190
|
+
return. Nothing in this skill runs it โ this skill writes nothing at all.
|
|
191
|
+
- **`headSha` is the commit you read** โ `git rev-parse HEAD` in the checkout
|
|
192
|
+
you examined. A premise check answers about a tree, and the tree moves.
|
|
193
|
+
|
|
84
194
|
## Examples โ the three shapes this actually catches
|
|
85
195
|
|
|
86
196
|
**The thing already exists.** Item: "the payload schema does not reject an empty
|
|
@@ -110,9 +220,10 @@ is invisible to every gate downstream.
|
|
|
110
220
|
about runtime behaviour ("this times out in production"), about intent, or
|
|
111
221
|
about a system this repository does not contain is `UNVERIFIABLE` here, not
|
|
112
222
|
false โ say so rather than guessing.
|
|
113
|
-
- **
|
|
114
|
-
|
|
115
|
-
(`.claude/rules/autonomy.md`), not this skill.
|
|
223
|
+
- **Each entry point is one pass, at its own end of the task.** A premise that goes
|
|
224
|
+
false *between* them โ a merge lands, a dependency moves โ is a staleness stop rule
|
|
225
|
+
(`.claude/rules/autonomy.md`), not this skill. Neither pass watches the other's
|
|
226
|
+
claims.
|
|
116
227
|
- **It has no opinion on whether the task is worth doing.** True premises and a
|
|
117
228
|
pointless task is a perfectly consistent state, and it belongs to whoever fills
|
|
118
229
|
the queue.
|