plumbbob 0.6.6 → 0.8.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/.claude-plugin/plugin.json +2 -2
- package/README.md +24 -7
- package/dist/cli-core.js +7 -2
- package/dist/lib/buildlog.js +5 -2
- package/dist/lib/check.js +33 -1
- package/dist/lib/git.js +17 -0
- package/dist/lib/latch.js +97 -0
- package/dist/lib/orient.js +24 -0
- package/dist/lib/sidecar.js +116 -0
- package/dist/verbs/build.js +5 -1
- package/dist/verbs/checkpoint.js +51 -2
- package/dist/verbs/doctor.js +33 -2
- package/dist/verbs/finish.js +57 -1
- package/dist/verbs/revert.js +19 -1
- package/dist/verbs/start.js +22 -2
- package/dist/verbs/status.js +10 -3
- package/dist/verbs/turn.js +106 -0
- package/hooks/hooks.json +15 -0
- package/hooks/pre-bash-commit.sh +69 -0
- package/node_modules/checkride/CHANGELOG.md +111 -0
- package/node_modules/checkride/README.md +185 -20
- package/node_modules/checkride/dist/adapters.d.ts +9 -4
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +62 -2
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/hook.d.ts +63 -0
- package/node_modules/checkride/dist/agent-setup/hook.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/hook.js +92 -0
- package/node_modules/checkride/dist/agent-setup/hook.js.map +1 -0
- package/node_modules/checkride/dist/agent-setup/index.d.ts +8 -0
- package/node_modules/checkride/dist/agent-setup/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/agent-setup/index.js +7 -0
- package/node_modules/checkride/dist/agent-setup/index.js.map +1 -0
- package/node_modules/checkride/dist/atomic.d.ts +13 -0
- package/node_modules/checkride/dist/atomic.d.ts.map +1 -0
- package/node_modules/checkride/dist/atomic.js +24 -0
- package/node_modules/checkride/dist/atomic.js.map +1 -0
- package/node_modules/checkride/dist/baseline/command.d.ts +42 -0
- package/node_modules/checkride/dist/baseline/command.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/command.js +58 -0
- package/node_modules/checkride/dist/baseline/command.js.map +1 -0
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +41 -0
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/fingerprint.js +119 -0
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -0
- package/node_modules/checkride/dist/baseline/index.d.ts +13 -0
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/index.js +10 -0
- package/node_modules/checkride/dist/baseline/index.js.map +1 -0
- package/node_modules/checkride/dist/baseline/store.d.ts +67 -0
- package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/store.js +110 -0
- package/node_modules/checkride/dist/baseline/store.js.map +1 -0
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +37 -3
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +43 -3
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +95 -22
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.d.ts +52 -0
- package/node_modules/checkride/dist/digest/digest.d.ts.map +1 -0
- package/node_modules/checkride/dist/digest/digest.js +147 -0
- package/node_modules/checkride/dist/digest/digest.js.map +1 -0
- package/node_modules/checkride/dist/digest/index.d.ts +8 -0
- package/node_modules/checkride/dist/digest/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/digest/index.js +7 -0
- package/node_modules/checkride/dist/digest/index.js.map +1 -0
- package/node_modules/checkride/dist/doctor.d.ts +4 -0
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +33 -10
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/index.d.ts +1 -1
- package/node_modules/checkride/dist/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/index.js +1 -1
- package/node_modules/checkride/dist/index.js.map +1 -1
- package/node_modules/checkride/dist/init.d.ts +38 -0
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +97 -8
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/orchestrator.d.ts +58 -1
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +155 -27
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pm/detect.d.ts +21 -0
- package/node_modules/checkride/dist/pm/detect.d.ts.map +1 -0
- package/node_modules/checkride/dist/pm/detect.js +60 -0
- package/node_modules/checkride/dist/pm/detect.js.map +1 -0
- package/node_modules/checkride/dist/pm/index.d.ts +10 -0
- package/node_modules/checkride/dist/pm/index.d.ts.map +1 -0
- package/node_modules/checkride/dist/pm/index.js +9 -0
- package/node_modules/checkride/dist/pm/index.js.map +1 -0
- package/node_modules/checkride/dist/pm/translate.d.ts +28 -0
- package/node_modules/checkride/dist/pm/translate.d.ts.map +1 -0
- package/node_modules/checkride/dist/pm/translate.js +40 -0
- package/node_modules/checkride/dist/pm/translate.js.map +1 -0
- package/node_modules/checkride/package.json +20 -17
- package/node_modules/checkride/schema/checkride.config.schema.json +97 -0
- package/node_modules/checkride/schema/checkride.summary.schema.json +57 -0
- package/node_modules/checkride/templates/shared/cspell.json +1 -0
- package/node_modules/checkride/templates/shared/prettierrc.json +6 -0
- package/package.json +8 -4
- package/skills/pb-build/SKILL.md +14 -0
- package/skills/pb-plan/SKILL.md +17 -0
- package/skills/pb-verify/SKILL.md +18 -0
package/dist/verbs/doctor.js
CHANGED
|
@@ -28,9 +28,10 @@ import { dirname, isAbsolute, join } from 'node:path';
|
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
import { runDoctor } from 'checkride';
|
|
30
30
|
import { listAgents } from "../lib/agents.js";
|
|
31
|
+
import { gateDetectsTools } from "../lib/check.js";
|
|
31
32
|
import { marketplacePlumbbob } from "../lib/plugins.js";
|
|
32
33
|
import { findRepoRoot, gitPath, stagePath } from "../lib/git.js";
|
|
33
|
-
import { buildDir, excludeControl, listBuilds, sidecarDir, slugify } from "../lib/sidecar.js";
|
|
34
|
+
import { buildDir, excludeControl, listBuilds, readTurn, sidecarDir, slugify } from "../lib/sidecar.js";
|
|
34
35
|
import { resolveString, settingsPath, setLocalSetting } from "../lib/settings.js";
|
|
35
36
|
// The plumbbob package's own skills/ dir (the canonical set), off this module's
|
|
36
37
|
// URL so it resolves the same from src/ (dev) and dist/ (published).
|
|
@@ -384,6 +385,15 @@ async function gateReport(cwd) {
|
|
|
384
385
|
if (c.required && c.status !== 'ok')
|
|
385
386
|
failed += 1;
|
|
386
387
|
}
|
|
388
|
+
// The week-1 bounce, called out where the human can see it coming
|
|
389
|
+
// (research/07 Build 2a): no CODE checks detected means checkpoints are
|
|
390
|
+
// gated only by checkride's always-on repo checks — either a vacuous
|
|
391
|
+
// refusal or, worse, a green that tested nothing. Informational, with the
|
|
392
|
+
// exact fix; `start` surfaces the same probe at plan time.
|
|
393
|
+
if (!gateDetectsTools(report.checks)) {
|
|
394
|
+
lines.push(' ○ gate: no code checks detected — checkpoints would be gated by the always-on repo checks alone;' +
|
|
395
|
+
' set {"check": "npm test"} in .plumbbob/settings.json, or add tool configs checkride can see');
|
|
396
|
+
}
|
|
387
397
|
return { lines, failed };
|
|
388
398
|
}
|
|
389
399
|
catch (err) {
|
|
@@ -402,6 +412,25 @@ function toolRow(c) {
|
|
|
402
412
|
const hint = c.status !== 'ok' && c.hint !== null ? `\n → ${c.hint}` : '';
|
|
403
413
|
return ` ${mark} ${c.slot ?? c.name} ← ${c.adapter}${version}${hint}`;
|
|
404
414
|
}
|
|
415
|
+
// The approval-latch health probe (D64): is the turn ledger live? `.plumbbob/TURN`
|
|
416
|
+
// holds a count once the UserPromptSubmit hook has ticked at least once, so a present
|
|
417
|
+
// count means the checkpoint latch is armed — its absence means the tick is
|
|
418
|
+
// guidance-only. Dormant is a legitimate state (a host with no hooks behaves exactly
|
|
419
|
+
// as it always has), never a failure — so this section adds nothing to the problem
|
|
420
|
+
// count. The dormant hint names the hook because a missing ledger almost always means
|
|
421
|
+
// it never wired, and that hook rides in either install kind (the marketplace
|
|
422
|
+
// plugin's hooks/hooks.json or an init-style link). Outside a repo there is no
|
|
423
|
+
// worktree ledger to read, so the section is skipped entirely.
|
|
424
|
+
function latchReport(cwd) {
|
|
425
|
+
const root = findRepoRoot(cwd);
|
|
426
|
+
if (root === null)
|
|
427
|
+
return { lines: [], failed: 0 };
|
|
428
|
+
const turn = readTurn(root);
|
|
429
|
+
const line = turn === null
|
|
430
|
+
? ' ○ latch: dormant — guidance only (no turn ledger yet; it ticks on your first prompt when the UserPromptSubmit hook is wired — re-run after one to confirm)'
|
|
431
|
+
: ` ✓ latch: live (turn ${turn})`;
|
|
432
|
+
return { lines: ['', 'plumbbob doctor — approval latch (D64)', line], failed: 0 };
|
|
433
|
+
}
|
|
405
434
|
export async function doctor(cwd, args = []) {
|
|
406
435
|
const checks = pluginChecks();
|
|
407
436
|
const out = ['plumbbob doctor — plugin install'];
|
|
@@ -414,7 +443,9 @@ export async function doctor(cwd, args = []) {
|
|
|
414
443
|
out.push(...agents.lines);
|
|
415
444
|
const gate = await gateReport(cwd);
|
|
416
445
|
out.push(...gate.lines);
|
|
417
|
-
const
|
|
446
|
+
const latch = latchReport(cwd);
|
|
447
|
+
out.push(...latch.lines);
|
|
448
|
+
const failed = checks.filter((c) => !c.ok).length + sidecar.failed + agents.failed + gate.failed + latch.failed;
|
|
418
449
|
out.push('');
|
|
419
450
|
out.push(failed === 0
|
|
420
451
|
? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code (or /reload-plugins).'
|
package/dist/verbs/finish.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { commit, findRepoRoot, isDirty, stageAll } from "../lib/git.js";
|
|
11
|
-
import { checkpointsPath, hasSession, intentPath, reportPath, resolveBuild, seamPath, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
|
|
11
|
+
import { checkpointsPath, clearTick, hasSession, intentPath, readStats, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
|
|
12
12
|
import { setLocalSetting } from "../lib/settings.js";
|
|
13
13
|
import { parseTitle } from "../lib/orient.js";
|
|
14
14
|
export function finish(cwd, args = []) {
|
|
@@ -20,6 +20,7 @@ export function finish(cwd, args = []) {
|
|
|
20
20
|
const { build: slug } = resolveBuild(root, args);
|
|
21
21
|
if (existsSync(reportPath(root, slug))) {
|
|
22
22
|
appendCheckpointShas(root, slug);
|
|
23
|
+
appendStats(root, slug);
|
|
23
24
|
}
|
|
24
25
|
else {
|
|
25
26
|
process.stderr.write('plumbbob: note — no report.md found; finishing without one ' +
|
|
@@ -40,6 +41,12 @@ export function finish(cwd, args = []) {
|
|
|
40
41
|
rmSync(seamPath(root, slug), { force: true });
|
|
41
42
|
rmSync(stepPath(root, slug), { force: true });
|
|
42
43
|
rmSync(spikePath(root, slug), { force: true });
|
|
44
|
+
// The latch's per-build entry stamp and the one-turn grant go with the session
|
|
45
|
+
// (D64/D65): a grant is one-turn by construction, but the session's last tick is
|
|
46
|
+
// the last time it was rewritten — left behind, a stale `auto` could self-approve
|
|
47
|
+
// the next session's first landing.
|
|
48
|
+
clearTick(root, slug);
|
|
49
|
+
setGrant(root, null);
|
|
43
50
|
if (slug !== null) {
|
|
44
51
|
// Drop the activeBuild key — JSON.stringify omits an `undefined` value, so the
|
|
45
52
|
// cursor is removed while the other local settings (auto, …) survive. Skipped
|
|
@@ -81,6 +88,55 @@ function bodyArg(args) {
|
|
|
81
88
|
return null;
|
|
82
89
|
}
|
|
83
90
|
}
|
|
91
|
+
// Roll the per-step receipts (research/07 Build 2b) into report.md as a `## Stats`
|
|
92
|
+
// table — one row per step plus totals, so "is the loop worth it?" is a table,
|
|
93
|
+
// not a feeling. Silently skipped when nothing accrued (an old build, a build
|
|
94
|
+
// with no red/revert/drift and no `build <n>` stamps has nothing to say).
|
|
95
|
+
function appendStats(root, slug) {
|
|
96
|
+
const stats = readStats(root, slug);
|
|
97
|
+
const steps = Object.keys(stats)
|
|
98
|
+
.map(Number)
|
|
99
|
+
.filter((n) => Number.isFinite(n))
|
|
100
|
+
.sort((a, b) => a - b);
|
|
101
|
+
if (steps.length === 0) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const totals = { redChecks: 0, driftWarnings: 0, reverts: 0, wallMs: 0 };
|
|
105
|
+
const rows = steps.map((n) => {
|
|
106
|
+
const s = stats[String(n)] ?? {};
|
|
107
|
+
totals.redChecks += s.redChecks ?? 0;
|
|
108
|
+
totals.driftWarnings += s.driftWarnings ?? 0;
|
|
109
|
+
totals.reverts += s.reverts ?? 0;
|
|
110
|
+
const wall = wallClockMs(s);
|
|
111
|
+
totals.wallMs += wall ?? 0;
|
|
112
|
+
return `| ${n} | ${s.redChecks ?? 0} | ${s.driftWarnings ?? 0} | ${s.reverts ?? 0} | ${formatWall(wall)} |`;
|
|
113
|
+
});
|
|
114
|
+
appendFileSync(reportPath(root, slug), [
|
|
115
|
+
'',
|
|
116
|
+
'## Stats',
|
|
117
|
+
'',
|
|
118
|
+
'| step | red checks | drift warnings | reverts | wall-clock |',
|
|
119
|
+
'|------|------------|----------------|---------|------------|',
|
|
120
|
+
...rows,
|
|
121
|
+
`| **total** | ${totals.redChecks} | ${totals.driftWarnings} | ${totals.reverts} | ${formatWall(totals.wallMs > 0 ? totals.wallMs : null)} |`,
|
|
122
|
+
'',
|
|
123
|
+
].join('\n'));
|
|
124
|
+
}
|
|
125
|
+
function wallClockMs(s) {
|
|
126
|
+
if (s.startedAt === undefined || s.landedAt === undefined) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const ms = Date.parse(s.landedAt) - Date.parse(s.startedAt);
|
|
130
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
131
|
+
}
|
|
132
|
+
// `—` when unknown (a hand-built step never ran `build <n>`, so it has no
|
|
133
|
+
// startedAt), `<1m` under a minute, whole minutes otherwise.
|
|
134
|
+
function formatWall(ms) {
|
|
135
|
+
if (ms === null) {
|
|
136
|
+
return '—';
|
|
137
|
+
}
|
|
138
|
+
return ms < 60_000 ? '<1m' : `${Math.round(ms / 60_000)}m`;
|
|
139
|
+
}
|
|
84
140
|
// Append the recorded checkpoints (baseline + each `step n <sha>`) to report.md as a
|
|
85
141
|
// `## Checkpoints` section, so the report — which now rides the branch into the PR —
|
|
86
142
|
// lists the SHAs. Best-effort: an unreadable checkpoints file yields an empty list.
|
package/dist/verbs/revert.js
CHANGED
|
@@ -18,7 +18,7 @@ import { tmpdir } from 'node:os';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
|
|
21
|
-
import { checkpointsPath, hasSession, resolveBuild, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
|
|
21
|
+
import { bumpStepStat, checkpointsPath, hasSession, resolveBuild, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
|
|
22
22
|
import { isArtifactPath, matchesSeam } from "../lib/intent.js";
|
|
23
23
|
export function revert(cwd, args) {
|
|
24
24
|
const root = findRepoRoot(cwd);
|
|
@@ -53,12 +53,19 @@ export function revert(cwd, args) {
|
|
|
53
53
|
// ignored files alone, so they must be removed explicitly afterward).
|
|
54
54
|
const seam = readSeamTokens(root, slug);
|
|
55
55
|
const toRemove = untrackedPaths(root).filter((p) => matchesSeam(p, seam) && !isArtifactPath(p));
|
|
56
|
+
// The dogfood receipt (research/07 2b): a revert against an in-flight step is a
|
|
57
|
+
// datapoint — read the marker before it goes, bump after the reset (the sidecar
|
|
58
|
+
// is preserved through it, so the write survives).
|
|
59
|
+
const inFlight = readInFlightStep(root, slug);
|
|
56
60
|
resetPreserving(root, sha, plumbbobOwnedPaths(root));
|
|
57
61
|
for (const rel of toRemove) {
|
|
58
62
|
rmSync(join(root, rel), { force: true, recursive: true });
|
|
59
63
|
}
|
|
60
64
|
rmSync(seamPath(root, slug), { force: true });
|
|
61
65
|
rmSync(stepPath(root, slug), { force: true });
|
|
66
|
+
if (inFlight !== null) {
|
|
67
|
+
bumpStepStat(root, slug, inFlight, 'reverts');
|
|
68
|
+
}
|
|
62
69
|
process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — back at the boundary. Park lines and intent edits were preserved.\n`);
|
|
63
70
|
return 0;
|
|
64
71
|
}
|
|
@@ -139,3 +146,14 @@ function readSeamTokens(root, slug) {
|
|
|
139
146
|
return [];
|
|
140
147
|
}
|
|
141
148
|
}
|
|
149
|
+
// The in-flight STEP number, or null when none — read for the revert receipt
|
|
150
|
+
// before the marker is cleared.
|
|
151
|
+
function readInFlightStep(root, slug) {
|
|
152
|
+
try {
|
|
153
|
+
const raw = readFileSync(stepPath(root, slug), 'utf8').trim();
|
|
154
|
+
return /^\d+$/.test(raw) ? Number(raw) : null;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/verbs/start.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { detectGate } from "../lib/check.js";
|
|
6
7
|
import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
|
|
7
|
-
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, } from "../lib/sidecar.js";
|
|
8
|
+
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
|
|
8
9
|
import { settingsPath, setLocalSetting } from "../lib/settings.js";
|
|
9
10
|
// D24/D32: the gate is checkride unless a `check` setting overrides it, so
|
|
10
11
|
// settings.json seeds with no `check` key at all — absence IS the default. The
|
|
11
12
|
// templates' {{CHECK}} line is a human-readable echo of that.
|
|
12
13
|
const CHECK_ECHO = 'checkride (set a "check" key in .plumbbob/settings.json to override)';
|
|
13
|
-
export function start(cwd, args) {
|
|
14
|
+
export async function start(cwd, args) {
|
|
14
15
|
const positionals = args.filter((a) => !a.startsWith('--'));
|
|
15
16
|
const allowDirty = args.includes('--allow-dirty');
|
|
16
17
|
const local = args.includes('--local');
|
|
@@ -75,7 +76,26 @@ export function start(cwd, args) {
|
|
|
75
76
|
excludeControl(root);
|
|
76
77
|
intentLocation = `.plumbbob/builds/${slug}/intent.md`;
|
|
77
78
|
}
|
|
79
|
+
// The plan's entry stamp (D64): `checkpoint --plan` latches on this tick. Skipped
|
|
80
|
+
// when TURN is absent — including the known first-session seam where the hook has
|
|
81
|
+
// never ticked, which leaves that one plan commit guidance-governed. Any grant
|
|
82
|
+
// lying around is a leftover from an earlier session (a legitimate one is minted
|
|
83
|
+
// by a `/pb-build` prompt, which never runs `start`) — clear it so a stale `auto`
|
|
84
|
+
// can't self-approve this session's plan (D65: one-turn lifetime).
|
|
85
|
+
setGrant(root, null);
|
|
86
|
+
stampTick(root, local ? null : slug);
|
|
78
87
|
process.stdout.write(`plumbbob: started "${title}" — baseline ${sha.slice(0, 9)}. Frame and decide in ${intentLocation}; \`build\` a step once the decisions are made.\n`);
|
|
88
|
+
// The plan-time gate probe (research/07 Build 2a): if checkride sees no code
|
|
89
|
+
// checks here, say so NOW — while the human is still deciding — instead of at
|
|
90
|
+
// the first checkpoint, where the gate either refuses a vacuous run or, worse,
|
|
91
|
+
// greens on the always-on repo checks alone. Guidance only: never the exit
|
|
92
|
+
// code (C1), and a configured `check` or a probe hiccup stays silent.
|
|
93
|
+
const gate = await detectGate(root);
|
|
94
|
+
if (gate.configured === null && !gate.detected) {
|
|
95
|
+
process.stderr.write('plumbbob: heads-up — the check gate sees no code checks in this repo (checkride detected no tools).\n' +
|
|
96
|
+
' Set one while you are still planning: add {"check": "npm test"} to .plumbbob/settings.json.\n' +
|
|
97
|
+
' Checkpoints gate on it — with nothing to run, the gate refuses or greens vacuously.\n');
|
|
98
|
+
}
|
|
79
99
|
return 0;
|
|
80
100
|
}
|
|
81
101
|
// Derived slugs carry a `YYYY-MM-DD-` prefix (local time) so `builds/` sorts
|
package/dist/verbs/status.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { findRepoRoot } from "../lib/git.js";
|
|
6
|
+
import { commitsSince, findRepoRoot } from "../lib/git.js";
|
|
7
7
|
import { SLOTS, readHarnessFile, resolveAgent, } from "../lib/agents.js";
|
|
8
8
|
import { buildFolder, buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, listBuilds, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
9
|
-
import { formatOrientation, orient } from "../lib/orient.js";
|
|
9
|
+
import { formatOrientation, lastLedgerSha, orient } from "../lib/orient.js";
|
|
10
10
|
function readOr(path) {
|
|
11
11
|
try {
|
|
12
12
|
return readFileSync(path, 'utf8');
|
|
@@ -33,12 +33,19 @@ export function status(cwd, args = []) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
const inFlightRaw = readOr(stepPath(root, slug)).trim();
|
|
36
|
+
// The out-of-band count (D66) needs git (orient is pure), so status measures it:
|
|
37
|
+
// commits on HEAD since the last ledger line's SHA — baseline, plan, or step, so
|
|
38
|
+
// a commit landing before the first step checkpoint still surfaces. No ledger yet
|
|
39
|
+
// ⇒ nothing to reconcile against ⇒ 0.
|
|
40
|
+
const checkpoints = readOr(checkpointsPath(root, slug));
|
|
41
|
+
const anchor = lastLedgerSha(checkpoints);
|
|
36
42
|
const orientation = orient({
|
|
37
43
|
intent: readOr(intentPath(root, slug)),
|
|
38
44
|
buildLog: readOr(buildLogPath(root, slug)),
|
|
39
|
-
checkpoints
|
|
45
|
+
checkpoints,
|
|
40
46
|
inFlight: /^\d+$/.test(inFlightRaw) ? Number(inFlightRaw) : null,
|
|
41
47
|
spiking: inSpike(root, slug),
|
|
48
|
+
outOfBand: anchor === null ? 0 : commitsSince(root, anchor),
|
|
42
49
|
});
|
|
43
50
|
const lines = [formatOrientation(orientation), ...harnessSection(root, slug)];
|
|
44
51
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// `plumbbob turn` — the turn-ledger hook (D64). Wired to Claude Code's
|
|
2
|
+
// UserPromptSubmit, it runs once per human prompt: it ticks `.plumbbob/TURN` (the
|
|
3
|
+
// monotonic count of human turns the checkpoint latch reads to know a human turn
|
|
4
|
+
// intervened since a step began) and rewrites `.plumbbob/GRANT` from the literal
|
|
5
|
+
// prompt — minting a one-turn self-approval only when the human typed
|
|
6
|
+
// `/pb-build --auto` or a `N-M` range (D65), clearing it otherwise. Pure machinery,
|
|
7
|
+
// not a user verb: it writes nothing to stdout and ALWAYS exits 0, so a broken tick
|
|
8
|
+
// can never wedge prompting (C3).
|
|
9
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { findSessionRoot, setGrant, turnPath } from "../lib/sidecar.js";
|
|
11
|
+
export function turn(cwd, _args) {
|
|
12
|
+
try {
|
|
13
|
+
applyTurn(cwd, readStdin());
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// C3: a broken turn must never wedge a prompt — swallow and exit 0.
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
// The testable core: apply one tick given the raw hook stdin. Split from the fd-0
|
|
21
|
+
// read so the unit tests can drive it directly — an in-process test cannot feed fd 0
|
|
22
|
+
// (the same constraint checkpoint's `--body` lives under).
|
|
23
|
+
export function applyTurn(cwd, raw) {
|
|
24
|
+
const root = findSessionRoot(cwd);
|
|
25
|
+
if (root === null)
|
|
26
|
+
return 0; // no active session above cwd — silent no-op.
|
|
27
|
+
writeFileSync(turnPath(root), `${readTurn(root) + 1}\n`);
|
|
28
|
+
setGrant(root, grantFromPrompt(extractPrompt(raw)));
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
// GRANT is minted only from a literal `pb-build` invocation the human typed:
|
|
32
|
+
// `pb-build` is disable-model-invocation, so this string can only come from a human
|
|
33
|
+
// keystroke, never model-authored pressure (D65). A bounded range beats `--auto`
|
|
34
|
+
// (bounded wins); a bare `/pb-build`, or any non-invocation prompt, mints nothing.
|
|
35
|
+
// The namespaced `/plumbbob:pb-build` form is honored alongside the bare one.
|
|
36
|
+
//
|
|
37
|
+
// Only the invocation's own arguments can mint: the tokens that follow it on its
|
|
38
|
+
// line, up to the first token that isn't argument-shaped (a flag, a step number, a
|
|
39
|
+
// range). An incidental range elsewhere in the prompt — an issue number, a pasted
|
|
40
|
+
// `2020-2024` — is prose, not a grant. Trailing sentence punctuation on an argument
|
|
41
|
+
// (`/pb-build 1-3.`) is still the argument the human typed.
|
|
42
|
+
export function grantFromPrompt(prompt) {
|
|
43
|
+
const invocation = /\/(?:plumbbob:)?pb-build\b/.exec(prompt);
|
|
44
|
+
if (invocation === null)
|
|
45
|
+
return null;
|
|
46
|
+
const line = prompt.slice(invocation.index + invocation[0].length).split('\n', 1)[0] ?? '';
|
|
47
|
+
let auto = false;
|
|
48
|
+
let ceiling;
|
|
49
|
+
for (const token of line.split(/\s+/)) {
|
|
50
|
+
const word = token.replace(/[.,;:!?]+$/, '');
|
|
51
|
+
if (word === '')
|
|
52
|
+
continue;
|
|
53
|
+
if (word === '--auto') {
|
|
54
|
+
auto = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const range = /^\d+-(\d+)$/.exec(word);
|
|
58
|
+
if (range !== null) {
|
|
59
|
+
ceiling = range[1];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (/^\d+$/.test(word) || word.startsWith('-'))
|
|
63
|
+
continue; // a step number or another flag
|
|
64
|
+
break; // free text — the arguments have ended
|
|
65
|
+
}
|
|
66
|
+
if (ceiling !== undefined)
|
|
67
|
+
return `range ${ceiling}`;
|
|
68
|
+
return auto ? 'auto' : null;
|
|
69
|
+
}
|
|
70
|
+
// Extract the `prompt` field from the UserPromptSubmit JSON, or '' when the input is
|
|
71
|
+
// absent, unparseable, or carries no string prompt. Malformed input contributes
|
|
72
|
+
// nothing (D27): the tick still lands (a human turn did occur) and the empty prompt
|
|
73
|
+
// clears any GRANT; the verb never throws over bad stdin.
|
|
74
|
+
function extractPrompt(raw) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
return typeof parsed.prompt === 'string' ? parsed.prompt : '';
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// The current TURN as a non-negative integer, or 0 when the ledger is absent or
|
|
84
|
+
// unreadable — so the first tick creates it at 1.
|
|
85
|
+
function readTurn(root) {
|
|
86
|
+
try {
|
|
87
|
+
const n = Number.parseInt(readFileSync(turnPath(root), 'utf8').trim(), 10);
|
|
88
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Read the hook JSON from fd 0. An interactive TTY never sends EOF, so skip the read
|
|
95
|
+
// there rather than hang (mirrors checkpoint's `--body`); a read error degrades to
|
|
96
|
+
// '' — extractPrompt then clears the grant and the tick still lands.
|
|
97
|
+
function readStdin() {
|
|
98
|
+
if (process.stdin.isTTY === true)
|
|
99
|
+
return '';
|
|
100
|
+
try {
|
|
101
|
+
return readFileSync(0, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/pre-bash-commit.sh\"" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
],
|
|
3
11
|
"PostToolUse": [
|
|
4
12
|
{
|
|
5
13
|
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
|
@@ -7,6 +15,13 @@
|
|
|
7
15
|
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/post-edit.sh\"" }
|
|
8
16
|
]
|
|
9
17
|
}
|
|
18
|
+
],
|
|
19
|
+
"UserPromptSubmit": [
|
|
20
|
+
{
|
|
21
|
+
"hooks": [
|
|
22
|
+
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/plumbbob\" turn" }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
10
25
|
]
|
|
11
26
|
}
|
|
12
27
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# pre-bash-commit.sh — the git-commit ask-hook (D66). PreToolUse on Bash: when a
|
|
3
|
+
# step is in flight and the model reaches for a raw `git commit`, turn that commit
|
|
4
|
+
# into a permission *question*, never a wall. checkpoint owns the landing (the latch
|
|
5
|
+
# lives there); this only nudges the human to route through it. ALWAYS exits 0 and
|
|
6
|
+
# never `deny`s — the human decides, C5 stays intact (D66). `plumbbob checkpoint`'s
|
|
7
|
+
# own internal `git commit` spawn never passes through hooks, so nothing self-trips.
|
|
8
|
+
|
|
9
|
+
# The repo root is the nearest ancestor with an active session — `.plumbbob/STATE`,
|
|
10
|
+
# the sentinel `start` writes and `finish` removes. Present in both the tracked and
|
|
11
|
+
# the `--local` layout (the activeBuild cursor post-edit.sh probes exists only in
|
|
12
|
+
# the tracked one, and this hook must guard `--local` steps too).
|
|
13
|
+
find_root() {
|
|
14
|
+
d=$(pwd -P)
|
|
15
|
+
while [ -n "$d" ]; do
|
|
16
|
+
if [ -f "$d/.plumbbob/STATE" ]; then
|
|
17
|
+
printf '%s' "$d"
|
|
18
|
+
return 0
|
|
19
|
+
fi
|
|
20
|
+
[ "$d" = "/" ] && break
|
|
21
|
+
d=$(dirname "$d")
|
|
22
|
+
done
|
|
23
|
+
return 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
root=$(find_root) || exit 0 # no active session here: nothing to guard
|
|
27
|
+
|
|
28
|
+
# The in-flight signal is STEP (cleared when the step checkpoints): inside the
|
|
29
|
+
# active build's folder when the cursor names one, else the flat sidecar STEP of
|
|
30
|
+
# the `--local` layout. jq if present, else the same sed fallback post-edit.sh
|
|
31
|
+
# uses for its field read.
|
|
32
|
+
settings="$root/.plumbbob/settings.local.json"
|
|
33
|
+
slug=$(jq -r '.activeBuild // empty' "$settings" 2>/dev/null)
|
|
34
|
+
[ -z "$slug" ] && slug=$(sed -n 's/.*"activeBuild"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$settings" 2>/dev/null | head -n1)
|
|
35
|
+
if [ -n "$slug" ]; then
|
|
36
|
+
step_file="$root/.plumbbob/builds/$slug/STEP"
|
|
37
|
+
else
|
|
38
|
+
step_file="$root/.plumbbob/STEP"
|
|
39
|
+
fi
|
|
40
|
+
[ -f "$step_file" ] || exit 0 # no step in flight: silent
|
|
41
|
+
|
|
42
|
+
input=$(cat)
|
|
43
|
+
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
44
|
+
[ -z "$cmd" ] && cmd=$(printf '%s' "$input" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
|
45
|
+
[ -z "$cmd" ] && exit 0
|
|
46
|
+
|
|
47
|
+
# A command *mentioning* "git commit" is prose, not a commit — a grep pattern, an
|
|
48
|
+
# echoed sentence, a checkpoint --body heredoc. Best-effort strip before matching:
|
|
49
|
+
# drop heredoc bodies (everything below the first `<<` line; the operator line
|
|
50
|
+
# itself still counts, so `git commit -F- <<MSG` is caught) and quoted string
|
|
51
|
+
# contents. A real commit that follows a heredoc body in the same command slips
|
|
52
|
+
# through unasked — acceptable for an affordance that never gates.
|
|
53
|
+
stripped=$(printf '%s\n' "$cmd" |
|
|
54
|
+
awk '{ print } /<</ { exit }' |
|
|
55
|
+
sed -e "s/'[^']*'//g" -e 's/"[^"]*"//g')
|
|
56
|
+
|
|
57
|
+
# Match a `git … commit` invocation: `git` as a command word (start or after a
|
|
58
|
+
# non-word char), then — without crossing a shell separator (`&& || ; |`) — the
|
|
59
|
+
# `commit` subcommand as a whole word. This clears `git status`/`git diff`/`git log`
|
|
60
|
+
# (no `commit` word) and `git log --grep=commit` (`=commit`, no space before it),
|
|
61
|
+
# while still catching `git -C path commit` and `git commit -m …`.
|
|
62
|
+
printf '%s' "$stripped" |
|
|
63
|
+
grep -Eq '(^|[^[:alnum:]_])git[[:space:]]+([^&|;]*[[:space:]])?commit([[:space:]]|$)' || exit 0
|
|
64
|
+
|
|
65
|
+
# A step is in flight and this is a raw commit: ask (never deny — the human decides).
|
|
66
|
+
# The reason is static, so a plain printf of the literal JSON needs no escaping.
|
|
67
|
+
printf '%s\n' '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "plumbbob: a step is in flight — checkpoint owns the landing. Approve only if you asked for this commit."}}'
|
|
68
|
+
|
|
69
|
+
exit 0
|
|
@@ -4,6 +4,114 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-07-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Vacuous-green signal.** `summary.json` gains a top-level `checks_run` count
|
|
12
|
+
of the checks that actually executed, so "green because everything passed" and
|
|
13
|
+
"green because nothing ran" are distinguishable by every consumer: `ok: true`
|
|
14
|
+
with `checks_run: 0` means nothing was verified. A zero-check run now prints a
|
|
15
|
+
loud warning naming why each slot sat out and how to enable it, and the new
|
|
16
|
+
`--strict` flag turns that case into exit 2 (for CI and commit-hook gates; the
|
|
17
|
+
default stays a warned exit 0 so exploring a fresh repo isn't punished).
|
|
18
|
+
`checks_run` is additive — `schema_version` is unchanged.
|
|
19
|
+
- **A frozen, tested contract.** `docs/contract.md` declares the surfaces
|
|
20
|
+
consumers may rely on — the exit-code taxonomy, the `summary.json`
|
|
21
|
+
additive-only discipline, the CLI flags, the programmatic exports, and the
|
|
22
|
+
pre-1.0 exact-pin policy — each locked by a new `test/contract/` suite that
|
|
23
|
+
fails the build on drift. The summary shape ships as a published JSON Schema
|
|
24
|
+
(`schema/checkride.summary.schema.json`).
|
|
25
|
+
- `DEFAULT_TIMEOUT_SECONDS` is now part of the public programmatic surface.
|
|
26
|
+
- New docs: a copy-paste CI guide (`docs/ci.md`), a reliability article
|
|
27
|
+
(`docs/reliability.md`), and `CONTRIBUTING.md` with the release ritual and
|
|
28
|
+
succession path.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Per-check timeouts are on by default** (600s; override per check or globally,
|
|
33
|
+
`0` to disable). A check that exceeds it is killed (SIGTERM → grace → SIGKILL)
|
|
34
|
+
and recorded as failed with a "timed out" note — a hung tool can no longer
|
|
35
|
+
hang the definition of done. Give long-running slots (`test`, `mutation`, …) a
|
|
36
|
+
higher cap or `0` on large repos.
|
|
37
|
+
- Run artifacts (`summary.json`, the raw slot files, the digest, the baseline)
|
|
38
|
+
are written atomically (temp file then rename), so a run interrupted mid-write
|
|
39
|
+
never leaves a consumer a half-written file to parse.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The supported Node floor is now stated consistently as `>=22.18` across the
|
|
44
|
+
docs; `docs/tools.md` and `docs/getting-started.md` previously claimed `>=24`,
|
|
45
|
+
contradicting `package.json` engines.
|
|
46
|
+
|
|
47
|
+
### Internal
|
|
48
|
+
|
|
49
|
+
- CI runs the full suite across macOS and Linux at the Node floor (22.18.0) and
|
|
50
|
+
current (24), and the e2e suite exercises all four package managers
|
|
51
|
+
(pnpm/npm/yarn/bun) plus an interrupted-run case. Releases now publish with npm
|
|
52
|
+
provenance, and the README wears the Stryker mutation score.
|
|
53
|
+
|
|
54
|
+
## [0.2.1] - 2026-07-08
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- Every `checkride init` scaffold shipped a `spell` check that failed out of the
|
|
59
|
+
box: the generated AGENTS.md contract stanza uses the word "baselined", but the
|
|
60
|
+
scaffolded `cspell.json` dictionary didn't include it, so a freshly generated
|
|
61
|
+
project's first `checkride` run exited non-zero.
|
|
62
|
+
|
|
63
|
+
### Internal
|
|
64
|
+
|
|
65
|
+
- Added a fast local guard (`generated-spell.test.ts`) that runs cspell against an
|
|
66
|
+
in-process `init` scaffold for each shape, catching this class of drift in
|
|
67
|
+
`pnpm check` instead of only in the slower end-to-end suite.
|
|
68
|
+
|
|
69
|
+
## [0.2.0] - 2026-07-08
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **Baseline** — adopt checkride on an existing repo without turning it into a
|
|
74
|
+
cleanup project. `checkride baseline` records current diagnostics into a
|
|
75
|
+
committed `checkride.baseline.json`; a normal run then passes a slot as long as
|
|
76
|
+
only baselined findings remain, fails listing only genuinely new ones, and
|
|
77
|
+
*ratchets* the file smaller as findings are fixed — never larger, and never
|
|
78
|
+
pruned on a partial `--only`/`--skip`/`--changed` run. `checkride init
|
|
79
|
+
--baseline` grandfathers today's debt instead of disabling failing slots.
|
|
80
|
+
Grandfathered counts surface in `.check/summary.json` as an additive
|
|
81
|
+
`baselined` field.
|
|
82
|
+
- **Package-manager-agnostic runs** — checkride detects pnpm, npm, yarn, or bun
|
|
83
|
+
(from the lockfile or the `packageManager` field) and translates each tool
|
|
84
|
+
invocation accordingly; the default pnpm behavior is byte-identical to before.
|
|
85
|
+
`doctor` reports the detected manager. The `security` audit stays pnpm-only
|
|
86
|
+
until per-manager adapters land.
|
|
87
|
+
- **`checkride agent-setup`** plus an `init` Stop hook — both write an idempotent
|
|
88
|
+
Claude Code Stop hook to `.claude/settings.json` that runs the gate on the
|
|
89
|
+
*detected* package manager and blocks a stop while checks are red. `agent-setup`
|
|
90
|
+
also (re)writes the AGENTS.md contract stanza for a repo set up without a full
|
|
91
|
+
`init`. Both are opt-out with `--no-hook`.
|
|
92
|
+
- **`format` slot** (opt-in) — a blessed `prettier` adapter (with `biome` as an
|
|
93
|
+
alternate) that runs before `lint` and is wired into `checkride fix`. Excluded
|
|
94
|
+
from the default run so upgrading never turns a repo red; `init` can enable it
|
|
95
|
+
for greenfield projects. The `order: "first"` custom-check hatch still works for
|
|
96
|
+
bespoke formatters.
|
|
97
|
+
- **`publint` and `attw` slots** (opt-in) — library-publishing checks that make
|
|
98
|
+
"the published package is correct" part of the definition of done: `publint`
|
|
99
|
+
lints the `package.json` publishing surface, `attw` verifies types resolve
|
|
100
|
+
across module systems. Detect-gated so apps that never publish don't run them.
|
|
101
|
+
- **Config presets via `extends`** — `checkride.config.json` accepts `"extends":
|
|
102
|
+
"<package-or-path>"` (string or array) to inherit a shared base; local keys win,
|
|
103
|
+
and a missing or circular extend fails with a friendly message.
|
|
104
|
+
- **`--digest`** — writes a token-bounded Markdown excerpt of the failing slots to
|
|
105
|
+
`.check/digest.md`, each section pointing at the authoritative raw output, so
|
|
106
|
+
agents spend less context triaging failures on large repos. Absent on a green
|
|
107
|
+
run.
|
|
108
|
+
- **Custom-check `detect` field** — a custom check can declare `detect:
|
|
109
|
+
["<file>"]` so a shared preset skips it when the file is absent and activates it
|
|
110
|
+
when present, keeping one config safe across heterogeneous repos.
|
|
111
|
+
- **Published JSON Schema** — `schema/checkride.config.schema.json` describes the
|
|
112
|
+
full config surface and ships in the package; `init` writes a version-pinned
|
|
113
|
+
`$schema` pointer into generated configs for editor validation.
|
|
114
|
+
|
|
7
115
|
## [0.1.6] - 2026-06-30
|
|
8
116
|
|
|
9
117
|
### Added
|
|
@@ -130,6 +238,9 @@ The first real release. (`0.0.0` was a name-claim placeholder.)
|
|
|
130
238
|
- Flags: `--only`, `--skip`, `--bail`, `--json`, `--changed`, `--all`,
|
|
131
239
|
`--include`.
|
|
132
240
|
|
|
241
|
+
[0.3.0]: https://www.npmjs.com/package/checkride/v/0.3.0
|
|
242
|
+
[0.2.1]: https://www.npmjs.com/package/checkride/v/0.2.1
|
|
243
|
+
[0.2.0]: https://www.npmjs.com/package/checkride/v/0.2.0
|
|
133
244
|
[0.1.6]: https://www.npmjs.com/package/checkride/v/0.1.6
|
|
134
245
|
[0.1.5]: https://www.npmjs.com/package/checkride/v/0.1.5
|
|
135
246
|
[0.1.4]: https://www.npmjs.com/package/checkride/v/0.1.4
|