Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .devkit/comment-firewall-rationales.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"version": 1,
"entries": {
"57344959ff31": {
"rationale": "Index-only evidence and parser-backed whole-token reconstruction are security boundaries spanning several Git and TypeScript helpers, not facts visible from one function name.",
"at": "2026-08-18T13:30:00.000Z"
},
"b1342df3a29b": {
"rationale": "The both-parent intersection is a non-obvious merge attribution rule required to avoid blaming inherited comments on the pending merge resolution.",
"at": "2026-08-18T13:30:00.000Z"
},
"ebba2fded6cb": {
"rationale": "Unified diff syntax makes three leading plus signs ambiguous; this records why the parser treats them as source only after entering a hunk.",
"at": "2026-08-18T13:30:00.000Z"
},
"52a8b780fbce": {
"rationale": "The caught MERGE_HEAD lookup failure is the intentional ordinary-commit branch, explaining why first-parent attribution remains complete rather than degraded.",
"at": "2026-08-18T13:30:00.000Z"
},
"565690146f1b": {
"rationale": "The second evidence read closes a stage-while-model-runs race; without this explanation it looks like redundant detection and is likely to be removed.",
"at": "2026-08-18T13:30:00.000Z"
},
"8c288304f3f4": {
"rationale": "These numeric exits are an ABI consumed by package, standalone, overlay, self-host, and strict-ship hook fragments, so their meanings must remain explicit.",
"at": "2026-08-18T13:30:00.000Z"
},
"f9c8533da784": {
"rationale": "A committed rationale is deliberately pending evidence rather than authorization; stating that boundary prevents future readers from treating the store as a bypass list.",
"at": "2026-08-18T13:30:00.000Z"
},
"4193b3475796": {
"rationale": "Reading staged bytes is the authorization boundary that prevents an unstaged rationale edit from changing whether the pending commit is approved.",
"at": "2026-08-18T13:30:00.000Z"
},
"012180f3b566": {
"rationale": "Missing evidence is a normal empty state, but corrupt staged evidence must block; this distinction prevents corruption from silently becoming authorization.",
"at": "2026-08-18T13:30:00.000Z"
},
"03992c8c06c9": {
"rationale": "The type alone cannot express that author evidence remains pending until an independent reviewer issues PASS, which is the central anti-self-waiver invariant.",
"at": "2026-08-18T13:30:00.000Z"
}
}
}
2 changes: 2 additions & 0 deletions .devkit/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"fanout",
"dup",
"clone",
"comments",
"decisions",
"qavis-advisory",
"review"
Expand All @@ -42,6 +43,7 @@
"fanout",
"dup",
"clone",
"comments",
"decisions",
"qavis-advisory",
"review"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ node_modules
.devkit/review-runs/
.devkit/last-ship-gates-*.log
.devkit/reconcile-manifest.json
.devkit/comment-firewall-receipts.json
# ...and the `.generation` sidecars those caches write. Untracked-but-not-ignored, they were enough
# to make `devkit release` refuse ("working tree not clean") in the repo that ships devkit itself.
.devkit/*.generation
Expand Down
22 changes: 22 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,28 @@ echo "🚧 Deterministic gates (aggregated)..."
__dk_no_git_env node gate-engine/deterministic/run.mts --hook "${DK_HOOK_PATH:-$0}" --structure "bun run lint:structure" --extra "lint=bun run lint" --extra "anti-slop=node cli/index.mts anti-slop check --staged" --extra "benchmarks=bun run benchmarks:check -- --mode staged" || exit 1
# /devkit:deterministic

if __dk_gate_selected comments; then
# devkit:guard-comments
echo "🧯 Changed-comment firewall..."
ccrc=0
__dk_no_git_env node gate-engine/comment-firewall/cli.mts gate || ccrc=$?
if [ "$ccrc" -eq 1 ]; then
exit 1
elif [ "$ccrc" -eq 4 ]; then
echo " guard-comments: NOT a rejection — staged comment evidence is unreadable or unsupported."
exit 1
elif [ "$ccrc" -eq 3 ]; then
echo " guard-comments: judge unavailable — strict ship mode failed closed."
echo " Check claude CLI auth/quota, then re-run devkit ship (approved comments are cached)."
exit 1
elif [ "$ccrc" -ne 0 ] && [ "$ccrc" -ne 2 ]; then
echo " guard-comments: unexpected exit $ccrc — blocking the commit."
exit 1
fi
# ccrc 0 = clean/approved/cached, 1 = unresolved/rejected, 2 = ordinary fail-open outage.
# /devkit:guard-comments
fi

if __dk_gate_selected decisions; then
# devkit:guard-decisions
echo "🧭 Decision-log gate..."
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ not accumulate in this config; they remain Markdown under `decisionsDir`.
- Folder fan-out, source-size, and project-structure ratchets
- Deterministic gate checkpointing for safe ship retries
- Sentry-capture review for swallowed runtime failures
- Changed-comment firewall with staged rationales and independent exception review
- Optional qavis advisory routing for UI changes

Every path the engine touches resolves from the consumer repository’s working directory. devkit ships mechanisms, not a consumer’s baselines, allowlists, decision history, or `guard.config.json`.
Expand Down Expand Up @@ -162,6 +163,7 @@ The tracker separates lifecycle, evidence provenance, freshness, change type, an
| Frontend security reviewer | shipped | accepted | stale | coverage | ? unknown | first-pass FAIL recall: 11/11 (100.0%) · first-pass clean pass: 7/8 (87.5%) · block recall: 10/11 (90.9%) · clean pass: 8/8 (100.0%) |
| Frontend performance reviewer | shipped | accepted | stale | coverage | ? unknown | first-pass FAIL recall: 10/11 (90.9%) · first-pass clean pass: 7/8 (87.5%) · block recall: 10/11 (90.9%) · clean pass: 7/8 (87.5%) |
| Correctness reviewer | shipped | accepted | stale | coverage | ↕ mixed | first-pass FAIL recall: 56/69 (81.2%) · first-pass clean pass: 49/59 (83.1%) |
| Changed-comment rationale review | shipped | evidence-only | unknown | — | ? unknown | No accepted local checkpoint |
| Decision governance | shipped | evidence-only | unknown | quality | ↑ improved | Detect accuracy: 45/49 (91.8%) · DECISION recall: 8/9 (88.9%) |
| Sentry capture judge | shipped | evidence-only | unknown | quality | ↑ improved | Commit-message F1: 56/100 (56.0%) · Focused-diff F1: 87/100 (87.0%) |
| Edge-case autonomy | no-ship | accepted | stale | no-ship | ? unknown | Judge-free ceiling: 51.2% · Pre-registered target: 35.0% |
Expand Down
11 changes: 10 additions & 1 deletion cli/__tests__/apply-init.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ describe('selection helpers', () => {
husky: true,
structure: true,
});
expect(s.guards).toEqual(['size', 'fanout', 'dup', 'clone', 'decisions', 'qavis-advisory']);
expect(s.guards).toEqual([
'size',
'fanout',
'dup',
'clone',
'comments',
'decisions',
'qavis-advisory',
]);
expect(s.agentTargets).toEqual(['claude', 'codex', 'cursor']);
});

Expand Down Expand Up @@ -797,6 +805,7 @@ describe('self-host mode (devkit dogfooding itself)', () => {
name: '@norvalbv/devkit',
bin: {
'guard-deterministic': './dist/gate-engine/deterministic/run.mjs',
'guard-comments': './dist/gate-engine/comment-firewall/cli.mjs',
'guard-decisions': './dist/gate-engine/decisions/cli.mjs',
'guard-review': './dist/gate-engine/review/cli.mjs',
'guard-qavis-advisory': './dist/gate-engine/qavis-advisory/cli.mjs',
Expand Down
7 changes: 1 addition & 6 deletions cli/__tests__/components-new-gates.test.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* The gate registry after promoting `qavis-advisory` to recommended, plus `newBundledGates` — the
* pure reconcile helper `upgrade` uses to detect gates a recorded selection predates (the fix for
* `devkit upgrade` silently dropping a newly-bundled gate).
*/
import { describe, expect, it } from 'vitest';
import { GUARD_IDS, newBundledGates, RECOMMENDED_GUARD_IDS } from '../lib/components.mts';

Expand All @@ -29,7 +24,7 @@ describe('newBundledGates', () => {
it('splits gates missing from a recorded selection into recommended vs opt-in', () => {
const recorded = ['size', 'fanout', 'dup', 'clone', 'decisions']; // a pre-qavis selection
const { recommended, optIn } = newBundledGates(recorded);
expect(recommended).toEqual(['qavis-advisory']); // newly recommended, absent
expect(recommended).toEqual(['comments', 'qavis-advisory']);
expect(optIn).toEqual(['review', 'sentry', 'coverage']); // bundled but never selected
});

Expand Down
4 changes: 3 additions & 1 deletion cli/__tests__/gitignore-cache.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ afterEach(() => {
describe('ensureDevkitCacheGitignore', () => {
it('manages the review run directory without ignoring tracked devkit state', () => {
expect(DEVKIT_CACHE_IGNORES).toContain('.devkit/review-runs/');
expect(DEVKIT_CACHE_IGNORES).toContain('.devkit/comment-firewall-receipts.json');
expect(DEVKIT_TRACKED_UNIGNORES).toContain('!.devkit/comment-firewall-rationales.json');
expect(DEVKIT_CACHE_IGNORES).not.toContain('.devkit/');
});

Expand Down Expand Up @@ -55,7 +57,7 @@ describe('ensureDevkitCacheGitignore', () => {
ensureDevkitCacheGitignore(d, false);

const lines = readFileSync(join(d, '.gitignore'), 'utf8').trimEnd().split('\n');
expect(lines.at(-1)).toBe(tracked);
expect(lines.slice(-DEVKIT_TRACKED_UNIGNORES.length)).toEqual(DEVKIT_TRACKED_UNIGNORES);
expect(lines.filter((line) => line === tracked)).toHaveLength(1);
});

Expand Down
30 changes: 28 additions & 2 deletions cli/__tests__/husky-block-exec.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ afterEach(() => {
while (homes.length) rmSync(homes.pop(), { recursive: true, force: true });
});

const ALL_GUARDS = ['size', 'fanout', 'dup', 'clone', 'decisions', 'review'];
const ALL_GUARDS = ['size', 'fanout', 'dup', 'clone', 'comments', 'decisions', 'review'];

// Hooks run under whatever /bin/sh the OS ships — dash on Debian/Ubuntu, bash on macOS. The
// fragments are POSIX sh; prove it where dash is installed instead of assuming.
Expand Down Expand Up @@ -55,6 +55,7 @@ tool="$1"; shift
echo "$tool $*" >> "$HOME/calls.log"
case "$tool" in
guard-deterministic) exit \${DET_RC:-0};;
guard-comments) exit \${COMMENTS_RC:-0};;
guard-decisions) exit \${DEC_RC:-0};;
guard-review)
case "$1" in
Expand Down Expand Up @@ -86,7 +87,13 @@ esac
let stdout = '';
try {
stdout = execFileSync(shell, ['-e', hookPath], {
env: { ...process.env, HOME: home, PATH: '/usr/bin:/bin', ...env },
env: {
...process.env,
DEVKIT_COMMIT_MSG_FILE: '',
HOME: home,
PATH: '/usr/bin:/bin',
...env,
},
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
Expand All @@ -110,6 +117,7 @@ describe('assembled hook execution (stubbed bunx, sh -e)', () => {
expect(r.status).toBe(1);
expect(r.calls).toContain('guard-deterministic');
// `guard-deterministic … || exit 1` — a doomed commit never pays for a judge.
expect(r.calls).not.toContain('guard-comments');
expect(r.calls).not.toContain('guard-decisions');
expect(r.calls).not.toContain('guard-review');
});
Expand All @@ -118,6 +126,7 @@ describe('assembled hook execution (stubbed bunx, sh -e)', () => {
const r = runHook({ DET_RC: '0' });
expect(r.status).toBe(0);
expect(r.calls).toContain('guard-deterministic');
expect(r.calls).toContain('guard-comments gate');
expect(r.calls).toContain('guard-decisions');
expect(r.calls).toContain('guard-review');
});
Expand Down Expand Up @@ -171,6 +180,23 @@ describe('assembled hook execution (stubbed bunx, sh -e)', () => {
expect(r.stdout).toContain('strict ship mode failed closed');
expect(r.stdout).not.toContain('Record the decision target');
});

it('guard-comments blocks before later AI gates on an unresolved finding', () => {
const r = runHook({ COMMENTS_RC: '1' });
expect(r.status).toBe(1);
expect(r.calls).toContain('guard-comments gate');
expect(r.calls).not.toContain('guard-decisions');
expect(r.calls).not.toContain('guard-review');
});

it('guard-comments distinguishes fail-open outage from strict/unreadable evidence', () => {
const r = runHook({ COMMENTS_RC: '2' });
expect(r.status).toBe(0);
expect(r.calls).toContain('guard-decisions');
expect(r.calls).toContain('guard-review');
expect(runHook({ COMMENTS_RC: '3' }).status).toBe(1);
expect(runHook({ COMMENTS_RC: '4' }).status).toBe(1);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

describe('parallel completeness prewarm (ship message file present)', () => {
Expand Down
2 changes: 2 additions & 0 deletions cli/__tests__/husky-block.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ describe('buildGuardBlock', () => {
expect(block).not.toContain('bunx guard-size');
expect(block).not.toContain('bunx guard-fanout');
// AI guards keep their own fail-fast fragments.
expect(block).toContain('bunx guard-comments gate');
expect(block).toContain('bunx guard-decisions');
expect(block).toContain('bunx guard-review');
expect(block).toContain('__dk_gate_selected comments');
expect(block).toContain('__dk_gate_selected decisions');
expect(block).toContain('__dk_gate_selected review');
expect(block).toContain('DEVKIT_REVIEW_GUARDS');
Expand Down
1 change: 1 addition & 0 deletions cli/__tests__/init-doctor.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ describe('init --yes (all recommended)', () => {
'fanout',
'dup',
'clone',
'comments',
'decisions',
'qavis-advisory',
]);
Expand Down
4 changes: 3 additions & 1 deletion cli/__tests__/review-cache-session.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe('review cache session', () => {
expect(prepared.status, prepared.stderr.toString()).toBe(0);
expect(prepared.stdout.toString().split('\0')).toEqual([
'devkit-review-cache-session-v1',
'4',
'5',
'review-cache.json',
'',
'decisions-verdict-cache.json',
Expand All @@ -158,6 +158,8 @@ describe('review cache session', () => {
'',
'sentry-verdict-cache.json',
'',
'comment-firewall-receipts.json',
'',
'',
]);

Expand Down
23 changes: 21 additions & 2 deletions cli/__tests__/self-host.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const HOOK_SEL = {
describe('self-host bin rewrite', () => {
it('sourceBinFor maps a guard bin to its source .mts (derived from package.json bin)', () => {
expect(sourceBinFor(ROOT, 'guard-review')).toBe('gate-engine/review/cli.mts');
expect(sourceBinFor(ROOT, 'guard-comments')).toBe('gate-engine/comment-firewall/cli.mts');
expect(sourceBinFor(ROOT, 'guard-deterministic')).toBe('gate-engine/deterministic/run.mts');
expect(sourceBinFor(ROOT, 'guard-qavis-advisory')).toBe('gate-engine/qavis-advisory/cli.mts');
});
Expand Down Expand Up @@ -74,7 +75,16 @@ describe('self-host bin rewrite', () => {
describe('selfHostSelection', () => {
it('is the recommended guard set PLUS review', () => {
const sel = selfHostSelection();
for (const g of ['size', 'fanout', 'dup', 'clone', 'decisions', 'qavis-advisory', 'review'])
for (const g of [
'size',
'fanout',
'dup',
'clone',
'comments',
'decisions',
'qavis-advisory',
'review',
])
expect(sel.guards).toContain(g);
expect(sel.husky).toBe(true);
expect(sel).toMatchObject({ oxc: true, antiSlop: true });
Expand Down Expand Up @@ -105,7 +115,16 @@ describe('selfHostSelection', () => {
// dogfood repo, which is the whole reason the selection was pinned in the first place.
it('keeps guards FIXED even when the config records a narrower set', () => {
const sel = selfHostSelection({ guards: ['size'] });
for (const g of ['size', 'fanout', 'dup', 'clone', 'decisions', 'qavis-advisory', 'review'])
for (const g of [
'size',
'fanout',
'dup',
'clone',
'comments',
'decisions',
'qavis-advisory',
'review',
])
expect(sel.guards).toContain(g);
});

Expand Down
7 changes: 7 additions & 0 deletions cli/__tests__/stray-gate-calls.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ describe('strayGateCalls', () => {
expect(found[0].line).toBe(6); // shebang, open marker, 2 block lines, close marker, then this
});

it('reports a duplicate changed-comment firewall call outside its managed fragment', () => {
const found = strayGateCalls(
hook('guard-comments gate', 'bunx guard-comments gate\nbunx guard-decisions detect --gate'),
);
expect(found.map((item) => item.bin)).toEqual(['guard-comments gate']);
});

it('ignores calls INSIDE the managed block — that is where they belong', () => {
expect(strayGateCalls(hook('echo done'))).toHaveLength(0);
});
Expand Down
6 changes: 6 additions & 0 deletions cli/lib/components.mts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const RECOMMENDED_GUARD_IDS = [
'fanout',
'dup',
'clone',
'comments',
'decisions',
'qavis-advisory',
];
Expand Down Expand Up @@ -143,6 +144,11 @@ export const GUARD_OPTIONS = [
{ id: 'fanout', label: 'fanout', hint: 'folder fan-out ratchet' },
{ id: 'dup', label: 'dup', hint: 'semantic duplication (search-code)' },
{ id: 'clone', label: 'clone', hint: 'verbatim copy-paste (jscpd)' },
{
id: 'comments',
label: 'comments',
hint: 'challenge changed comments; explicit rationale gets independent Haiku review',
},
{ id: 'decisions', label: 'decisions', hint: 'architectural-decision log gate' },
{ id: 'review', label: 'review', hint: 'in-chain reviewer judges (sonnet → opus; model spend)' },
{
Expand Down
1 change: 1 addition & 0 deletions cli/lib/doctor/stray-gate-calls.mts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface StrayGateCall {
// is (almost always) a hand-written copy that predates devkit owning the gate.
const DEVKIT_GATE_BINS = [
'guard-deterministic',
'guard-comments',
'guard-decisions',
'guard-review',
'guard-qavis-advisory',
Expand Down
Loading
Loading