From c12286be444acb65ea56d4d3f0b7d666a9983213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:07:43 +0200 Subject: [PATCH 01/44] fix(fd3): validate the branch's own worktree and reject edited-to-green CI A toolchain report pinned the main checkout's absolute path as every command's cwd, so CI runners graded the root branch while reporting a stacked one, and a runner that sed-fixed a type error returned passed=true on an uncommitted tree. --- plugins/fd3/CHANGELOG.md | 11 +++ plugins/fd3/agents/toolchain-scout.md | 7 +- plugins/fd3/workflows/implement-run.js | 92 +++++++++++++++++++++----- plugins/fd3/workflows/repair-run.js | 84 ++++++++++++++++++----- 4 files changed, 161 insertions(+), 33 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 688a27f..7eadfa7 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- CI verdicts now describe the branch they claim to: the toolchain scout reports each command's + `cwd` relative to the repository root, the CI prompt `cd`s into the worktree and reads + `git branch --show-current`, and `implement-run` / `repair-run` discard a verdict whose branch + is not the unit's — previously an absolute `cwd` sent every command into the repository's main + checkout, so stacked branches were marked done on another branch's code +- A CI runner that edits its way to green no longer produces a pass: regenerating a derived + artifact counts as fixing, the runner returns `git status --porcelain`, and a verdict from a + tree carrying uncommitted changes beyond the task files is discarded as `no-verdict` + ## [0.1.0] - 2026-09-04 ### Added diff --git a/plugins/fd3/agents/toolchain-scout.md b/plugins/fd3/agents/toolchain-scout.md index 9a90b2a..a8abed6 100644 --- a/plugins/fd3/agents/toolchain-scout.md +++ b/plugins/fd3/agents/toolchain-scout.md @@ -74,7 +74,7 @@ Package manager: — — Validation commands (in order): -1. — cwd: — — source: +1. — cwd: — — source: scoped form: ` or `` placeholder — or "not scopeable: "> 2. ... @@ -86,6 +86,11 @@ Doubts: - ``` +Every `cwd` is **relative to the repository root** — `.`, `backend`, `packages/api`, never the +absolute path of the checkout you inspected. The caller runs your commands in git worktrees of +this repository, so an absolute path sends every command into the checkout you happened to read +and the branch under validation is never exercised. + Order the commands as CI orders them; where CI is silent, install → build → typecheck → lint → unit tests. Always include the install command, marked "required in a fresh worktree": the caller runs these commands in git worktrees, which share nothing installed — `node_modules` and diff --git a/plugins/fd3/workflows/implement-run.js b/plugins/fd3/workflows/implement-run.js index c0df7d6..0ea59ea 100644 --- a/plugins/fd3/workflows/implement-run.js +++ b/plugins/fd3/workflows/implement-run.js @@ -174,8 +174,10 @@ const baselinePrompt = (repo) => `1. Create a worktree at ${worktreePath(repo, 'baseline')} from ${repoDefault(repo)}`, ` (git worktree add ) unless it already exists — then reuse it as is.`, `2. Run every runnable validation command from the toolchain report below, in the reported`, - ` order, sequentially — never in parallel. Skip what the report lists as not runnable here,`, - ` recording each skip under skipped with its reason — a skip is never recorded as passed.`, + ` order, sequentially — never in parallel. Each command's cwd in the report is relative to`, + ` the repository root: resolve it inside that worktree, never against ${repo}. Skip what the`, + ` report lists as not runnable here, recording each skip under skipped with its reason — a`, + ` skip is never recorded as passed.`, `3. Fix nothing, change nothing. Run each command once, as \` 2>&1; echo "exit $?"\``, ` — that one run gives both the output and the exit status. Record, per command, whether it`, ` exited 0, and for each failure the output lines that matter.`, @@ -561,10 +563,12 @@ await baselineReady const CI_RESULT = { type: 'object', - required: ['passed', 'failures'], + required: ['passed', 'failures', 'branch', 'dirty'], properties: { passed: { type: 'boolean', description: 'true when nothing fails beyond the baseline' }, failures: { type: 'array', items: { type: 'string' }, description: 'one entry per newly failing command, with the load-bearing output lines' }, + branch: { type: 'string', description: '`git branch --show-current` in the worktree, read before the first command; `detached` when HEAD is detached' }, + dirty: { type: 'string', description: '`git status --porcelain` in the worktree after the last command, verbatim; an empty string when the tree is clean' }, preExisting: { type: 'array', items: { type: 'string' }, description: 'failures that match the baseline of the clean base — informational, never fixed on this branch' }, skipped: { type: 'array', items: { type: 'string' }, description: 'commands not run, each with the reason — a skip is never reported as passed' }, marked: { type: 'boolean', description: 'the task files were set to done; asked for on a final gate only' }, @@ -592,7 +596,15 @@ const ciPrompt = (unit, mode, markFiles) => [ `Run the validation commands for the repository ${unit.repo}, branch ${unit.branch},`, `in the worktree ${unit.worktree}. Run them in the reported order, sequentially — never in`, - `parallel. Toolchain report for this repository:`, + `parallel.`, + ``, + `First \`cd ${unit.worktree}\`, then run \`git branch --show-current\` and return its output`, + `as branch. Every command runs from there: each command's cwd in the report is relative to`, + `the repository root, so resolve it inside this worktree — never against ${unit.repo}, which`, + `is a different checkout on a different branch. If the branch you read is not ${unit.branch},`, + `run nothing: return it as branch with passed=false and say so in failures.`, + ``, + `Toolchain report for this repository:`, ``, toolchain.get(unit.repo), ``, @@ -608,9 +620,14 @@ const ciPrompt = (unit, mode, markFiles) => ``, `Skip everything the report lists as not runnable here, and skip a command the baseline`, `shows failing before it produces a verdict — re-proving a baseline failure is wasted time.`, - `Every skip goes under skipped with its reason; a skip is never reported as passed. Do not`, - `fix anything. A failure whose location and message match the baseline is pre-existing:`, - `return it under preExisting, never under failures, and do not count it against the branch.`, + `Every skip goes under skipped with its reason; a skip is never reported as passed. A failure`, + `whose location and message match the baseline is pre-existing: return it under preExisting,`, + `never under failures, and do not count it against the branch.`, + ``, + `Do not fix anything. Editing a source file, applying a formatter, and regenerating a derived`, + `artifact a command compares against — an index, a schema, a lockfile — are all fixing: report`, + `the failure and leave it. A verdict is only worth what the tree it ran on was, so when the`, + `last command has run, \`git status --porcelain\` and return its output verbatim as dirty.`, `Return passed=true only when every runnable command exits 0 or fails only on baseline`, `entries; otherwise return each newly failing command with the output lines that matter.`, ...(markFiles @@ -681,6 +698,43 @@ const reviewPrompt = (unit, skillName) => const mechanical = { model: 'haiku', effort: 'high' } // CI runners interpret command output; they design nothing +// A CI verdict is a statement about one tree at one commit. A runner that stayed in the +// repository's main checkout graded another branch's code, and one that edited its way to green +// graded a state no commit holds — both are absence of evidence, never a pass. +const taskFilePaths = new Set(tasks.map((t) => t.file)) +const porcelainPath = (line) => { + const p = line.length > 3 ? line.slice(3) : '' + const renamed = p.indexOf(' -> ') + return (renamed === -1 ? p : p.slice(renamed + 4)).replace(/^"|"$/g, '') +} +// The task files are the run's state store and the CI agent itself flips them to done, so their +// own dirtiness is expected; anything else in the tree is the runner's edit. +const strayChanges = (dirty, worktree) => + (dirty || '') + .split('\n') + .filter((line) => line.trim()) + .map(porcelainPath) + .filter((p) => p && !taskFilePaths.has(`${worktree}/${p}`)) + +const ciFault = (ci, unit) => { + const ran = (ci.branch || '').trim() + if (ran && ran !== unit.branch) return `ran in a checkout on ${ran} instead of ${unit.branch}` + if (!ran) return `could not name the branch it ran on` + const stray = strayChanges(ci.dirty, unit.worktree) + if (stray.length > 0) { + const shown = stray.slice(0, 5).join(', ') + return `left ${stray.length} uncommitted change(s) in the worktree (${shown}${stray.length > 5 ? ', …' : ''}), so its verdict describes a tree no commit holds` + } + return null +} + +const runCi = async (unit, mode, markFiles, label) => { + const ci = await tryTwice(ciPrompt(unit, mode, markFiles), { label, phase: 'Validate', schema: CI_RESULT, ...mechanical }) + if (!ci) return { ci: null, fault: null } + const fault = ciFault(ci, unit) + return { ci, fault } +} + const validation = [] // per-branch summary for the final report const REFRESH_RESULT = { @@ -728,20 +782,22 @@ for (const unit of units) { } } - let ci = await tryTwice(ciPrompt(unit, 'scoped', false), { label: `ci:${tag}`, phase: 'Validate', schema: CI_RESULT, ...mechanical }) - while (ci && !ci.passed && summary.fixRounds < maxFixRounds) { + let { ci, fault } = await runCi(unit, 'scoped', false, `ci:${tag}`) + while (ci && !fault && !ci.passed && summary.fixRounds < maxFixRounds) { summary.fixRounds += 1 const fix = await tryTwice(fixPrompt(unit, ci.failures, 'CI'), { label: `fix-ci:${tag}#${summary.fixRounds}`, phase: 'Validate', schema: FIX_RESULT }) if (fix && fix.caveats) caveats.push(...fix.caveats.map((c) => `${unit.branch} fix-ci: ${c}`)) - ci = await tryTwice(ciPrompt(unit, 'scoped', false), { label: `ci:${tag}#${summary.fixRounds + 1}`, phase: 'Validate', schema: CI_RESULT, ...mechanical }) + ;({ ci, fault } = await runCi(unit, 'scoped', false, `ci:${tag}#${summary.fixRounds + 1}`)) } - if (!ci) { + if (!ci || fault) { summary.ci = 'no-verdict' hil.push({ slug: null, kind: 'no-verdict', stage: 'ci', - reason: `${unit.repo} ${unit.branch}: the CI agent returned no result after a retry (transient API failure); the branch has no verdict after ${summary.fixRounds} fix rounds — absence of evidence, not a failure.`, + reason: fault + ? `${unit.repo} ${unit.branch}: the CI agent ${fault}; its verdict was discarded after ${summary.fixRounds} fix rounds — the branch is unvalidated, not failing.` + : `${unit.repo} ${unit.branch}: the CI agent returned no result after a retry (transient API failure); the branch has no verdict after ${summary.fixRounds} fix rounds — absence of evidence, not a failure.`, }) continue } @@ -776,22 +832,24 @@ for (const unit of units) { } // The full command list is the branch's final gate — always, review fixes or not. - let finalCi = await tryTwice(ciPrompt(unit, 'full', deadLenses.length === 0), { label: `ci:${tag}:final`, phase: 'Validate', schema: CI_RESULT, ...mechanical }) - if (finalCi && !finalCi.passed) { + let { ci: finalCi, fault: finalFault } = await runCi(unit, 'full', deadLenses.length === 0, `ci:${tag}:final`) + if (finalCi && !finalFault && !finalCi.passed) { // One fix round here: a final-gate failure is often mechanical — a derived artifact the // review fixes invalidated — and only what survives the round deserves a human. summary.fixRounds += 1 const fix = await tryTwice(fixPrompt(unit, finalCi.failures, 'final-gate CI'), { label: `fix-final:${tag}`, phase: 'Validate', schema: FIX_RESULT }) if (fix && fix.caveats) caveats.push(...fix.caveats.map((c) => `${unit.branch} fix-final: ${c}`)) - finalCi = await tryTwice(ciPrompt(unit, 'full', deadLenses.length === 0), { label: `ci:${tag}:final#2`, phase: 'Validate', schema: CI_RESULT, ...mechanical }) + ;({ ci: finalCi, fault: finalFault } = await runCi(unit, 'full', deadLenses.length === 0, `ci:${tag}:final#2`)) } - if (!finalCi) { + if (!finalCi || finalFault) { summary.ci = 'no-verdict' hil.push({ slug: null, kind: 'no-verdict', stage: 'ci-final', - reason: `${unit.repo} ${unit.branch}: scoped CI passed but the full-gate agent returned no result after a retry; the branch has no final verdict.`, + reason: finalFault + ? `${unit.repo} ${unit.branch}: scoped CI passed but the full-gate agent ${finalFault}; its verdict was discarded and the branch has no final verdict.` + : `${unit.repo} ${unit.branch}: scoped CI passed but the full-gate agent returned no result after a retry; the branch has no final verdict.`, }) continue } diff --git a/plugins/fd3/workflows/repair-run.js b/plugins/fd3/workflows/repair-run.js index cd9e92f..f0ab582 100644 --- a/plugins/fd3/workflows/repair-run.js +++ b/plugins/fd3/workflows/repair-run.js @@ -147,8 +147,10 @@ const baselinePrompt = (repo) => `1. Create a worktree at ${worktreePath(repo, 'baseline')} from ${repoDefault(repo)}`, ` (git worktree add ) unless it already exists — then reuse it as is.`, `2. Run every runnable validation command from the toolchain report below, in the reported`, - ` order, sequentially — never in parallel. Skip what the report lists as not runnable here,`, - ` recording each skip under skipped with its reason — a skip is never recorded as passed.`, + ` order, sequentially — never in parallel. Each command's cwd in the report is relative to`, + ` the repository root: resolve it inside that worktree, never against ${repo}. Skip what the`, + ` report lists as not runnable here, recording each skip under skipped with its reason — a`, + ` skip is never recorded as passed.`, `3. Fix nothing, change nothing. Run each command once, as \` 2>&1; echo "exit $?"\``, ` — that one run gives both the output and the exit status. Record, per command, whether it`, ` exited 0, and for each failure the output lines that matter.`, @@ -260,10 +262,12 @@ await baselineReady const CI_RESULT = { type: 'object', - required: ['passed', 'failures'], + required: ['passed', 'failures', 'branch', 'dirty'], properties: { passed: { type: 'boolean', description: 'true when nothing fails beyond the baseline' }, failures: { type: 'array', items: { type: 'string' }, description: 'one entry per newly failing command, with the load-bearing output lines' }, + branch: { type: 'string', description: '`git branch --show-current` in the worktree, read before the first command; `detached` when HEAD is detached' }, + dirty: { type: 'string', description: '`git status --porcelain` in the worktree after the last command, verbatim; an empty string when the tree is clean' }, preExisting: { type: 'array', items: { type: 'string' }, description: 'failures that match the baseline of the clean base — informational, never fixed on this branch' }, marked: { type: 'boolean', description: 'the task files were set to done; asked for on a final gate only' }, }, @@ -273,7 +277,15 @@ const ciPrompt = (unit, mode, markFiles) => [ `Run the validation commands for the repository ${unit.repo}, branch ${unit.branch},`, `in the worktree ${unit.worktree}. Run them in the reported order, sequentially — never in`, - `parallel. Toolchain report for this repository:`, + `parallel.`, + ``, + `First \`cd ${unit.worktree}\`, then run \`git branch --show-current\` and return its output`, + `as branch. Every command runs from there: each command's cwd in the report is relative to`, + `the repository root, so resolve it inside this worktree — never against ${unit.repo}, which`, + `is a different checkout on a different branch. If the branch you read is not ${unit.branch},`, + `run nothing: return it as branch with passed=false and say so in failures.`, + ``, + `Toolchain report for this repository:`, ``, toolchain.get(unit.repo), ``, @@ -289,9 +301,14 @@ const ciPrompt = (unit, mode, markFiles) => ``, `Skip everything the report lists as not runnable here, and skip a command the baseline`, `shows failing before it produces a verdict — re-proving a baseline failure is wasted time.`, - `Every skip goes under skipped with its reason; a skip is never reported as passed. Do not`, - `fix anything. A failure whose location and message match the baseline is pre-existing:`, - `return it under preExisting, never under failures, and do not count it against the branch.`, + `Every skip goes under skipped with its reason; a skip is never reported as passed. A failure`, + `whose location and message match the baseline is pre-existing: return it under preExisting,`, + `never under failures, and do not count it against the branch.`, + ``, + `Do not fix anything. Editing a source file, applying a formatter, and regenerating a derived`, + `artifact a command compares against — an index, a schema, a lockfile — are all fixing: report`, + `the failure and leave it. A verdict is only worth what the tree it ran on was, so when the`, + `last command has run, \`git status --porcelain\` and return its output verbatim as dirty.`, `Return passed=true only when every runnable command exits 0 or fails only on baseline`, `entries; otherwise return each newly failing command with the output lines that matter.`, ...(markFiles @@ -345,6 +362,39 @@ const fixPrompt = (unit, problems) => const mechanical = { model: 'haiku', effort: 'high' } // CI runners interpret command output; they design nothing +// A CI verdict is a statement about one tree at one commit. A runner that stayed in the +// repository's main checkout graded another branch's code, and one that edited its way to green +// graded a state no commit holds — both are absence of evidence, never a pass. +const porcelainPath = (line) => { + const p = line.length > 3 ? line.slice(3) : '' + const renamed = p.indexOf(' -> ') + return (renamed === -1 ? p : p.slice(renamed + 4)).replace(/^"|"$/g, '') +} +const ciFault = (ci, unit) => { + const ran = (ci.branch || '').trim() + if (ran && ran !== unit.branch) return `ran in a checkout on ${ran} instead of ${unit.branch}` + if (!ran) return `could not name the branch it ran on` + // The final gate flips this branch's task files to done itself, so their own dirtiness is + // expected wherever the tasks directory happens to live; anything else is the runner's edit. + const own = new Set(unit.taskFiles || []) + const stray = (ci.dirty || '') + .split('\n') + .filter((line) => line.trim()) + .map(porcelainPath) + .filter((p) => p && !own.has(`${unit.worktree}/${p}`)) + if (stray.length > 0) { + const shown = stray.slice(0, 5).join(', ') + return `left ${stray.length} uncommitted change(s) in the worktree (${shown}${stray.length > 5 ? ', …' : ''}), so its verdict describes a tree no commit holds` + } + return null +} + +const runCi = async (unit, mode, markFiles, label) => { + const ci = await tryTwice(ciPrompt(unit, mode, markFiles), { label, phase: 'Validate', schema: CI_RESULT, ...mechanical }) + if (!ci) return { ci: null, fault: null } + return { ci, fault: ciFault(ci, unit) } +} + const validation = [] // per-branch summary for the final report for (const unit of units) { @@ -357,20 +407,22 @@ for (const unit of units) { continue } - let ci = await tryTwice(ciPrompt(unit, 'scoped', false), { label: `ci:${tag}`, phase: 'Validate', schema: CI_RESULT, ...mechanical }) - while (ci && !ci.passed && summary.fixRounds < maxFixRounds) { + let { ci, fault } = await runCi(unit, 'scoped', false, `ci:${tag}`) + while (ci && !fault && !ci.passed && summary.fixRounds < maxFixRounds) { summary.fixRounds += 1 const fix = await tryTwice(fixPrompt(unit, ci.failures), { label: `fix-ci:${tag}#${summary.fixRounds}`, phase: 'Validate', schema: FIX_RESULT }) if (fix && fix.caveats) caveats.push(...fix.caveats.map((c) => `${unit.branch} fix-ci: ${c}`)) - ci = await tryTwice(ciPrompt(unit, 'scoped', false), { label: `ci:${tag}#${summary.fixRounds + 1}`, phase: 'Validate', schema: CI_RESULT, ...mechanical }) + ;({ ci, fault } = await runCi(unit, 'scoped', false, `ci:${tag}#${summary.fixRounds + 1}`)) } - if (!ci) { + if (!ci || fault) { summary.ci = 'no-verdict' hil.push({ slug: null, kind: 'no-verdict', stage: 'ci', - reason: `${unit.repo} ${unit.branch}: the CI agent returned no result after a retry (transient API failure); the branch has no verdict after ${summary.fixRounds} fix rounds — absence of evidence, not a failure.`, + reason: fault + ? `${unit.repo} ${unit.branch}: the CI agent ${fault}; its verdict was discarded after ${summary.fixRounds} fix rounds — the branch is unvalidated, not failing.` + : `${unit.repo} ${unit.branch}: the CI agent returned no result after a retry (transient API failure); the branch has no verdict after ${summary.fixRounds} fix rounds — absence of evidence, not a failure.`, }) continue } @@ -386,14 +438,16 @@ for (const unit of units) { } // The full command list is the branch's final gate — repairs go out only fully validated. - const finalCi = await tryTwice(ciPrompt(unit, 'full', !!(unit.taskFiles && unit.taskFiles.length > 0)), { label: `ci:${tag}:final`, phase: 'Validate', schema: CI_RESULT, ...mechanical }) - if (!finalCi) { + const { ci: finalCi, fault: finalFault } = await runCi(unit, 'full', !!(unit.taskFiles && unit.taskFiles.length > 0), `ci:${tag}:final`) + if (!finalCi || finalFault) { summary.ci = 'no-verdict' hil.push({ slug: null, kind: 'no-verdict', stage: 'ci-final', - reason: `${unit.repo} ${unit.branch}: scoped CI passed but the full-gate agent returned no result after a retry; the branch has no final verdict.`, + reason: finalFault + ? `${unit.repo} ${unit.branch}: scoped CI passed but the full-gate agent ${finalFault}; its verdict was discarded and the branch has no final verdict.` + : `${unit.repo} ${unit.branch}: scoped CI passed but the full-gate agent returned no result after a retry; the branch has no final verdict.`, }) continue } From 53d593da9d684c358f5bb3d2c1c6beca6d54f7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:09:06 +0200 Subject: [PATCH 02/44] =?UTF-8?q?fix(code-review):=20hold=20the=20Scanner?= =?UTF-8?q?=20protocol=20=E2=80=94=20end=20the=20turn,=20one=20note,=20one?= =?UTF-8?q?=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two runs burned their context polling instead of ending the turn, one of them killing a working security lens with TaskStop; seven tailored conventions notes turned the brief into per-lens instructions; and a Scanner that spawned its own agent overwrote its own findings. --- plugins/code-review/CHANGELOG.md | 13 +++++++ plugins/code-review/commands/start-cr.md | 46 ++++++++++++++++++++---- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index dfb29b0..19986ba 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -10,6 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Author metadata now reads `Mateusz Gostański ` in `plugin.json` and the marketplace entry. +- Step 3 spells out that waiting for the Scanners means ending the turn: no `sleep`, `ListAgents` + polling, transcript `stat`s, placeholder calls or `Monitor`/`until` loops, and never `TaskStop` + on a Scanner — elapsed time is not a state the Orchestrator can observe +- The conventions note is one byte-identical, suppress-only text in every brief, with each rule + quoted verbatim from its file: no per-Lens threat hypotheses, no "do not raise" lists, no + paraphrases +- A Scanner dispatches no agent of its own, waits in no background, and its final message is its + whole output; a `` presented as an amendment or a partial list counts as truncated and + the Lens is re-dispatched +- The unavailable-lens question offers exactly proceed-without-it or abort — reviewing that lens + inline is not an option on it +- Scanner search is described tool-neutrally (`Grep`, or `git grep` where sub-agents have no + `Grep` tool) ## [0.3.0] - 2026-09-02 diff --git a/plugins/code-review/commands/start-cr.md b/plugins/code-review/commands/start-cr.md index e4bb9be..fa3aec6 100644 --- a/plugins/code-review/commands/start-cr.md +++ b/plugins/code-review/commands/start-cr.md @@ -104,6 +104,15 @@ conflict the `.local` one wins. Work it there, then: records a **tracked `.local` file** (`git check-ignore` fails on it) and any **conflict between two project files** (resolved by scope.md's precedence order), both of which reach the report's `Conventions` line; +- **one note, byte-identical in every brief, and it may only suppress.** Write it once and paste + the same text into all N briefs: a per-Lens note is a per-Lens instruction, and the Scanner + reads whatever it finds there as what you want it to look for. So the slot holds nothing but + documented conventions, each **quoted verbatim with its file** — never your own threat + hypotheses or "where to focus", never an "established facts — do not raise" list, never a + paraphrase of a rule (one run's paraphrase said a legacy pattern "is documented as accepted" + where the rule said to migrate off it, and buried the very finding the user later asked for). + Anything you want checked belongs in the Lens's own rules file, not here. A note that grows + past a screen is the wrong shape: cut it to the rules that actually suppress something; - name any family or rule the language makes **N/A** in that note, so its owning Scanner clears it in one line instead of inventing findings to fit; - keep the standards text **out of the note**: it travels in the brief's own @@ -167,14 +176,28 @@ its findings verbatim inside `` — that is the delivery, and it arrives control straight back, so "ask and block on the reply" is not a thing the tool can do. Chasing a Scanner that is merely slow makes it regenerate its whole output, which can land after you have already merged. + + **Waiting is ending your turn.** Once the pre-reading below is done, say "standing by" and + end the turn: each `` wakes you, and a turn you never end is the only way + to *not* receive them promptly. Never `sleep`, never poll `ListAgents`, never `stat` a + Scanner's transcript, never emit a placeholder tool call to stay alive, and never set up a + `Monitor` or an `until` loop over any of these — a run that polled its way through the wait + burned 70% of its turns and two thirds of its context on `echo ok`, and the leftover timers + then fired into the report and the apply phase. And **never `TaskStop` a Scanner**: elapsed + time is not a state you can observe, the "stalled" one was mid-`Read` with 27 tool calls + behind it, and killing it cost the review its whole security lens. 2. **Fail closed on an empty ``, not on silence.** The failure to catch is a notification whose `` is missing, empty, or truncated mid-block — that Scanner - has **not** reported. Re-dispatch that one Lens as a fresh **unnamed** `Agent` and + has **not** reported. A `` that presents itself as an **amendment, a correction, or + a partial list** counts as truncated too, whatever it contains: the Scanner's own full + findings are somewhere you cannot see, so re-dispatch that Lens rather than merge the + fragment. Re-dispatch that one Lens as a fresh **unnamed** `Agent` and collect its `` the same way — this holds for every active Lens, `security`, `performance` and `spec` included. Never quietly review that lens yourself and pass the result off as a full N-lens review. If the re-dispatch also comes back empty, **tell the user that lens is unavailable** and ask whether to proceed without it or - abort. A single-pass or missing-lens review is a **labelled, user-acknowledged + abort — those two are the whole menu, and "I read that lens inline myself" is not on it, + however reasonable it looks as the recommended option. A single-pass or missing-lens review is a **labelled, user-acknowledged degradation**, never the silent default — that silent fallback is exactly how a single perspective's false positive reaches the report unchecked. 3. **Merge only once all N have delivered a ``.** Merging early loses findings. @@ -220,12 +243,21 @@ Send each Scanner a brief in this shape, filling every slot: Read the rules file **completely first**, then judge only the families that belong to that Lens. A Scanner **returns findings/verdicts only**: it does not render a report, does not re-grade centrally, and **writes nothing into the tree** — not the files under -review, and not a scratch or probe file to test a hypothesis against. It is reading the +review, and not a scratch or probe file to test a hypothesis against. + +A Scanner is **one agent, one pass, one output**. It **dispatches no agent of its own** — a +sub-agent puts a second hop between the finding and the merge, and the Scanner that tried it +had its own report overwritten by the follow-up, losing a handoff outright. It does not wait in +the background, poll, or schedule anything; it reads, judges, and returns. Its **final message +is its whole output**: if something has to change after it has already written its findings, +it re-sends the complete list, never an "amendment" or a delta — anything the last message +leaves out never reaches the merge. It is reading the user's working copy, so it settles a doubt by reading the type, the signature, or the call site, and marks the rest `(verify)`. Read the whole changed file for context, and target what the change touched. The `naming & module` Scanner alone adds the **one-hop -cross-file protocol** on top of that: Grep the importers of each changed module and the -imports of each module it newly imports, open those files at the matched lines only — +cross-file protocol** on top of that: search the importers of each changed module and the +imports of each module it newly imports — with the `Grep` tool, or `git grep` from `Bash` in a +session where that tool is not handed to sub-agents — open those files at the matched lines only — no transitive crawl, no repo listing, no `find`; a fact beyond the hop is `(verify)`; it still writes nothing. @@ -264,8 +296,8 @@ it still writes nothing. of the **source** (where untrusted data enters) and of the **sink**; a pattern alone (`req.body`, a string containing `SELECT`) is never a finding; `L` lists both ends, source first, and the clause says which is which. When either end sits - outside the files in view the Scanner reads it — it has `Read` and `Grep` — and marks - only what it still cannot confirm `(verify)`. `CANDIDATES` is reserved for a + outside the files in view the Scanner reads it — it can `Read` any file and search with + `Grep` or `git grep` — and marks only what it still cannot confirm `(verify)`. `CANDIDATES` is reserved for a confirmed source→sink pair whose *mitigation* is the doubt; a cleared look-alike is one prose line for `Not flagged`. Severity is `high` or `medium`, **never `nit`**. It never runs the code, an audit tool, or a network command; `.env`, YAML, JSON and From 3dc72fc0a887c4d602bc62c6450f703834e3d16d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:09:46 +0200 Subject: [PATCH 03/44] fix(code-review): tie the reconciliation counts to the blocks they name Six of twelve runs published arithmetic that added up over categories that did not: handoffs counted as merged into bullets that were never rendered, "0 boy-scout" over a block holding three of them, and primary findings dropping out of the merge uncounted entirely. --- plugins/code-review/CHANGELOG.md | 3 +++ plugins/code-review/commands/start-cr.md | 33 ++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 19986ba..cb1b955 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 inline is not an option on it - Scanner search is described tool-neutrally (`Grep`, or `git grep` where sub-agents have no `Grep` tool) +- The `Reconciliation` line gained a `P primary dropped` term and each of its counts now names + the rendered block it is checked against (`C` against `Boy-scout`, `D + P` against `Not + flagged`); `Not flagged` entries stay countable so the check can be verified from the report ## [0.3.0] - 2026-09-02 diff --git a/plugins/code-review/commands/start-cr.md b/plugins/code-review/commands/start-cr.md index fa3aec6..aafac27 100644 --- a/plugins/code-review/commands/start-cr.md +++ b/plugins/code-review/commands/start-cr.md @@ -429,12 +429,28 @@ One terse line each. Omit a block when it is empty. against each name its home — the report bullet (`path:line`) it became, the converging finding it merged into, or the `Not flagged` line that clears it. An entry with no home on that list is a bug: route it before you render. +- **A primary finding is reconciled too.** The channels are not the only thing that goes + missing: a Scanner's own `FINDINGS` entry can fall out of the merge between collecting and + rendering, and nothing downstream notices. Count what you received per Scanner, and give every + primary finding that does not reach a report bullet — deduped into another, demoted, or + rejected — its own `Not flagged` entry with the reason. Dedup is the one silent case allowed, + and only because the surviving bullet carries it. - **Publish that check as one counted line above the report** — `Reconciliation: N - handoffs + M candidates → A merged · B own bullet · C boy-scout · D Not flagged` — - where `A + B + C + D` equals `N + M`. The arithmetic is what makes the check real: a + handoffs + M candidates → A merged · B own bullet · C boy-scout · D Not flagged; P primary + dropped` — where `A + B + C + D` equals `N + M`, and `P` counts the primary findings that got + no bullet. The arithmetic is what makes the check real: a run that states "every handoff routed" without it has asserted rather than reconciled, and loses the entry nothing else corroborates. When the sums disagree, an entry is unrouted — find it, never adjust a number to close the gap. +- **Each count names the block it is counted in**, so the line can be checked against the report + rather than believed: `merged` is an entry folded into another finding's bullet and visible in + its text, `own bullet` one that became its own graded bullet under a file, `boy-scout` one + rendered in the `Boy-scout` block, `Not flagged` one rendered as its own entry in `Not + flagged`. Runs whose arithmetic was right have still printed `0 boy-scout` over a Boy-scout + block holding three routed handoffs, and counted six entries as `merged` into a bullet that + was never rendered. Before publishing, count the rendered blocks: `C` equals the Boy-scout + entries that came from a channel, and `D + P` equals the entries in `Not flagged`. A count + that does not match the block it names is the bug, not the block. - **Resolve every `(verify)` finding**: read the code and confirm or refute it. A confirmed finding drops the marker and proceeds; a refuted one is a **Scanner false positive** — drop it and note it under `Not flagged`. An unresolved `(verify)` finding @@ -462,7 +478,7 @@ comment verdicts **together**. Render with **exactly this template**, in this order — keep the structure identical between runs: ```markdown -Reconciliation: handoffs + candidates → merged · own bullet · boy-scout · Not flagged +Reconciliation: handoffs + candidates → merged · own bullet · boy-scout · Not flagged;

primary dropped ## Code review — @@ -488,7 +504,7 @@ each when one is a real problem with no rule to land on; omit when empty> A filled-in report reads like this: -Reconciliation: 4 handoffs + 2 candidates → 3 merged · 1 own bullet · 0 boy-scout · 2 Not flagged +Reconciliation: 4 handoffs + 2 candidates → 3 merged · 1 own bullet · 0 boy-scout · 2 Not flagged; 0 primary dropped ## Code review — committed (base → HEAD), 3 files @@ -552,10 +568,11 @@ Rules for filling it in: text; for MOVE, name the destination. - **Quote comments verbatim.** Every comment verdict carries the verbatim comment text and its `path:line`. -- **`Not flagged`** lists the look-alikes deliberately passed on, plus every candidate - and `HANDOFF` the merge cleared — one line when they are all genuine non-findings, a - short bullet each when one of them is a *real* problem that merely has no rule to land - on. A real problem keeps its own bullet rather than being compressed into a +- **`Not flagged`** lists the look-alikes deliberately passed on, plus every candidate, + `HANDOFF` and dropped primary finding the merge cleared — one line when they are all genuine + non-findings, a short bullet each when one of them is a *real* problem that merely has no rule + to land on. **Its entries stay countable**: separated by `;` on the one-line form, one bullet + each otherwise, because the `Reconciliation` line's last two numbers are checked against them. A real problem keeps its own bullet rather than being compressed into a subordinate clause; that compression is how something worth acting on disappears. Drop the block if empty. - **`Boy-scout`** holds only findings in code the change did not touch; omit the From 878f8ecd38035a0fc78799d43187dff3714d44c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:10:23 +0200 Subject: [PATCH 04/44] feat(code-review): check the fix before offering it, and sort boy-scout by risk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five runs put a behaviour-changing fix in front of the user unexamined — a guard moved onto a DTO, a payload bounded ahead of redaction, a client split that dropped a submit guard — and the batched boy-scout bucket is what carried the worst of them. --- plugins/code-review/CHANGELOG.md | 7 +++++ plugins/code-review/commands/start-cr.md | 34 ++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index cb1b955..38a3bf6 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 inline is not an option on it - Scanner search is described tool-neutrally (`Grep`, or `git grep` where sub-agents have no `Grep` tool) +- Step 4 now judges the fix as well as the finding — behaviour preserved, no contradiction with + another fix, no new smell — and re-routes a fix that fails any of the three to the structural + walk (with the behaviour change named) or to report-only +- Boy-scout extras are sorted by risk: a structural or `security` boy-scout fix walks one at a + time instead of riding the batch; a `Not flagged` item reaches the menu only as its own named + option; and the slot order puts `security`, then verified rule-less correctness problems, ahead + of boy-scout extras - The `Reconciliation` line gained a `P primary dropped` term and each of its counts now names the rendered block it is checked against (`C` against `Boy-scout`, `D + P` against `Not flagged`); `Not flagged` entries stay countable so the check can be verified from the report diff --git a/plugins/code-review/commands/start-cr.md b/plugins/code-review/commands/start-cr.md index aafac27..a629bac 100644 --- a/plugins/code-review/commands/start-cr.md +++ b/plugins/code-review/commands/start-cr.md @@ -467,6 +467,22 @@ One terse line each. Omit a block when it is empty. no fixed row: re-grade it against that mapping by re-reading the rule it quotes, not the Scanner's guess. A single-lens Scanner is the one most prone to the anchoring that table forbids, so its severity is a first pass and yours is the one that ships. +- **Judge the fix, not only the finding.** A finding can be right and its fix wrong, and Step 6 + is too late to notice: by then the user has approved it. For every fix that could reach a + bucket, check three things against the code you already read: + - **Does it keep behaviour?** Moving a guard onto a DTO turns a 400 into a 422; splitting a + shared client drops the double-submit guard that shared instance provided; deleting an unused + export removes what a later stage of the same spec consumes. A fix that changes what callers + observe is not mechanical, whatever its rule says. + - **Does it contradict another finding?** One review's headline fix bounded a payload *before* + the redaction walk, which would have truncated secrets under the redactor's minimum length — + a security hole introduced by a performance fix. Read the fixes as a set, not one at a time. + - **Does it create the next finding?** An extraction that takes five positional parameters, a + helper that duplicates one two files away — fix the fix before offering it. + + A fix that fails any of the three is re-routed: to the structural walk with the behaviour + change named in its option, or to report-only with one line on why. Say which in the report's + bullet rather than silently dropping the finding. - **Comment verdicts are not re-graded** and are **not** mapped to severities. The two vocabularies stay side by side; there is no severity↔verdict mapping anywhere in this command. @@ -632,7 +648,14 @@ menu; never add a fifth. `Report only` is always offered: `misplaced-logic`, `canonical-helper`, `pass-through`, `feature-envy`, `data-clump`, `message-chain`); every **`performance`** fix; every **`security`** fix; **plus** comment **MOVE**. -- **Boy-scout extras** — apply the untouched-code findings, or skip them. +- **Boy-scout extras** — apply the untouched-code findings, or skip them. **Risk sorts this + bucket too.** Only the mechanical ones — the same edits Safe fixes accepts — travel as a batch; + a boy-scout finding whose fix moves, removes or restructures code, or touches `security`, joins + the structural walk and is applied one at a time with its own yes. Untouched code is where the + review understands the least, so a structural edit there is riskier than the same edit inside + the diff, not safer: one run bundled a client split into this bucket, silently broke a + double-submit guard, dragged an unrelated page into the pull request, and the user discarded + the work. - **Report only** — change nothing. **Route any unlisted rule by the fix's risk, not its family:** a mechanical, eyeball-able @@ -670,7 +693,14 @@ must stay honest when findings don't spread across them: `Not flagged` or spans untouched code, yet the review actually verified — is offer-able as its own apply bucket; so is a verified `spec` · wrong-implementation with a one-edit fix. The review's most valuable output belongs in the menu, not buried in `Report - only` or `Boy-scout extras` because it lacks a rule tag. + only` or `Boy-scout extras` because it lacks a rule tag. It is the **only** way a `Not + flagged` item enters the menu: it gets its own option, named for the problem, never folded + into `Safe fixes` or `Boy-scout extras` where the user approves it without seeing it. +- **When there are more candidates than slots**, the order is: a confirmed `security` problem + first, then a verified correctness problem with no rule, then the canonical buckets by risk, + and `Boy-scout extras` last — it is the one whose loss costs the change nothing. A run that + gave its last slot to a boy-scout nit while a verified backend gap waited had the priority + backwards. - A before/after **preview** diff belongs in an `AskUserQuestion` option, never in the report body — Step 5 stays clause-only. From 054a65845def8f7c18fc8202796f97ed07ec2389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:12:34 +0200 Subject: [PATCH 05/44] fix(fd3): validate the parked branch at its commit, not in the user's checkout The root branch's worktree was the main checkout, so every status flip dirtied the tree under test and the user's own uncommitted work rode along in the verdict. --- plugins/fd3/CHANGELOG.md | 7 +++ plugins/fd3/skills/implement-tasks/SKILL.md | 11 ++++- plugins/fd3/workflows/implement-run.js | 52 +++++++++++++++------ plugins/fd3/workflows/repair-run.js | 45 +++++++++++++----- 4 files changed, 87 insertions(+), 28 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 7eadfa7..e073120 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -17,6 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A CI runner that edits its way to green no longer produces a pass: regenerating a derived artifact counts as fixing, the runner returns `git status --porcelain`, and a verdict from a tree carrying uncommitted changes beyond the task files is discarded as `no-verdict` +- A target branch that is the repository's own checkout is now validated in a detached worktree + at the branch's commit, so the user's uncommitted work and the tasks directory are no longer + part of the tree under test; merges and fixes still happen in the checkout +- The merge and CI prompts no longer claim the task files live outside the repository — they say + what actually holds: edit them at their absolute paths, commit nothing, touch nothing else +- `implement-tasks` says that a pre-launch commit of the spec and tasks directory must carry the + repository's regenerated indexes, or say it did not — a stale one fails every branch at once ## [0.1.0] - 2026-09-04 diff --git a/plugins/fd3/skills/implement-tasks/SKILL.md b/plugins/fd3/skills/implement-tasks/SKILL.md index 3b2fad0..98a11b7 100644 --- a/plugins/fd3/skills/implement-tasks/SKILL.md +++ b/plugins/fd3/skills/implement-tasks/SKILL.md @@ -133,6 +133,12 @@ One batch, following `${CLAUDE_SKILL_DIR}/../../references/question-batching.md` Everything else — wave composition, branch names, merge order — the task files already decided; report it, do not ask. +Committing the spec and the tasks directory before launch is the user's call, and it is a change +to the repository like any other: whatever that repository derives from the tree you touched — +a docs index, a manifest, a generated list — regenerate it in the same commit, or say plainly +that you did not. A stale generated file fails validation on every branch of the run at once, +and reads there as the branches' own defect. + ### 3. Launch Launch the dynamic workflow and let it run in the background: @@ -158,7 +164,10 @@ repository's `defaultRef` — the ref the user confirmed in step 2, fetched fres Worktrees and target branches are cut from that ref (or the task's stack base). When step 2 established that a target branch is the branch the repository itself is parked on, say so via `parkedBranch` — git refuses a second worktree for it, and the workflow must know to use the -main checkout rather than discover the refusal. On a relaunch, pass `reportPath` — the `` +main checkout rather than discover the refusal. Merges and fixes for that branch then happen in +the checkout, but its validation does not: the workflow grades it in a detached worktree beside +the repository, so the verdict describes the branch's commit rather than whatever else the user +has open in that tree. On a relaunch, pass `reportPath` — the `` path from the previous run's completion notification. The workflow reads that file's toolchain and baseline knowledge with one cheap agent, so the run skips a re-scout and a re-baseline of every repository it already knows. Never transcribe that knowledge into the call yourself: it is tens of diff --git a/plugins/fd3/workflows/implement-run.js b/plugins/fd3/workflows/implement-run.js index 0ea59ea..c68d36d 100644 --- a/plugins/fd3/workflows/implement-run.js +++ b/plugins/fd3/workflows/implement-run.js @@ -379,9 +379,9 @@ const mergePrompt = (repo, repoTasks) => { ` "Already up to date" is expected on a resumed run — count its task as merged.`, `4. For every task now merged in, including one that was already up to date, set`, ` \`status: merged\` in the task file listed beside it, changing nothing else in that file.`, - ` The task files are the run's state store, they live outside the repository, and they are`, - ` never committed — a status left at implemented after the merge is the one thing a later`, - ` reader cannot tell from a merge that never happened.`, + ` The task files are the run's state store and you never commit them, wherever they live —`, + ` a status left at implemented after the merge is the one thing a later reader cannot tell`, + ` from a merge that never happened.`, ``, `Resolve a merge conflict only when the two sides are clearly compatible and the resolution`, `is mechanical; commit the resolution and record it under resolved — the task's slug and one`, @@ -592,17 +592,37 @@ const CR_RESULT = { }, } -const ciPrompt = (unit, mode, markFiles) => - [ +// A branch that is checked out in the repository itself has no worktree of its own, and that +// checkout is the user's: their uncommitted work, and often the tasks directory, sit in the tree +// the commands would grade. Validate it in a detached worktree at the branch's commit instead — +// `git worktree add --detach` is allowed for a branch checked out elsewhere, and what it holds is +// exactly what the branch holds. Fixes still land in the checkout; the next run refreshes this one. +const validationTree = (unit) => + unit.worktree === unit.repo ? `${unit.repo}.worktrees/${unit.branch.replace(/\//g, '-')}-validate` : unit.worktree + +const ciPrompt = (unit, mode, markFiles) => { + const tree = validationTree(unit) + return [ `Run the validation commands for the repository ${unit.repo}, branch ${unit.branch},`, - `in the worktree ${unit.worktree}. Run them in the reported order, sequentially — never in`, + `in the worktree ${tree}. Run them in the reported order, sequentially — never in`, `parallel.`, ``, - `First \`cd ${unit.worktree}\`, then run \`git branch --show-current\` and return its output`, - `as branch. Every command runs from there: each command's cwd in the report is relative to`, - `the repository root, so resolve it inside this worktree — never against ${unit.repo}, which`, - `is a different checkout on a different branch. If the branch you read is not ${unit.branch},`, - `run nothing: return it as branch with passed=false and say so in failures.`, + ...(tree === unit.worktree + ? [] + : [ + `That worktree is this branch's validation checkout, detached at its commit. Create it`, + `with \`git worktree add --detach ${tree} ${unit.branch}\` if it is not there; if it is,`, + `bring it to the branch's current commit with \`git -C ${tree} checkout --detach`, + `${unit.branch}\`. Never \`git clean\` it — installed dependencies live there untracked.`, + ``, + ]), + `\`cd ${tree}\` before anything else, and confirm what you are about to grade: \`git rev-parse`, + `HEAD\` there must equal \`git -C ${unit.repo} rev-parse ${unit.branch}\`. When they match,`, + `return branch "${unit.branch}". When they do not, run nothing: return the branch you actually`, + `found (or the short HEAD sha when detached) as branch, with passed=false and the mismatch in`, + `failures. Every command runs from that worktree: each command's cwd in the report is relative`, + `to the repository root, so resolve it there — never against ${unit.repo}, which is a`, + `different checkout on a different branch.`, ``, `Toolchain report for this repository:`, ``, @@ -637,12 +657,14 @@ const ciPrompt = (unit, mode, markFiles) => `\`status: done\` in the frontmatter of these task files, changing nothing else in them,`, `and return marked=true:`, ...unit.tasks.map((slug) => `- ${tasks.find((t) => t.slug === slug).file}`), - `They are this run's state store: they live outside the repository, they are never`, - `committed, and the no-fixing rule above is about the code, not about them. On any`, - `failure leave them untouched and return marked=false.`, + `They are this run's state store, and the no-fixing rule above is about the code, not`, + `about them: edit them at the absolute paths listed, commit nothing, and if they happen`, + `to sit inside a checkout of this repository, leave that checkout's other files alone.`, + `On any failure leave them untouched and return marked=false.`, ] : []), ].join('\n') +} const fixPrompt = (unit, problems, source) => [ @@ -720,7 +742,7 @@ const ciFault = (ci, unit) => { const ran = (ci.branch || '').trim() if (ran && ran !== unit.branch) return `ran in a checkout on ${ran} instead of ${unit.branch}` if (!ran) return `could not name the branch it ran on` - const stray = strayChanges(ci.dirty, unit.worktree) + const stray = strayChanges(ci.dirty, validationTree(unit)) if (stray.length > 0) { const shown = stray.slice(0, 5).join(', ') return `left ${stray.length} uncommitted change(s) in the worktree (${shown}${stray.length > 5 ? ', …' : ''}), so its verdict describes a tree no commit holds` diff --git a/plugins/fd3/workflows/repair-run.js b/plugins/fd3/workflows/repair-run.js index f0ab582..2ad2720 100644 --- a/plugins/fd3/workflows/repair-run.js +++ b/plugins/fd3/workflows/repair-run.js @@ -273,17 +273,35 @@ const CI_RESULT = { }, } -const ciPrompt = (unit, mode, markFiles) => - [ +// A branch checked out in the repository itself has no worktree of its own, and that checkout is +// the user's: their uncommitted work sits in the tree the commands would grade. Validate it in a +// detached worktree at the branch's commit — repairs still land in the checkout. +const validationTree = (unit) => + unit.worktree === unit.repo ? `${unit.repo}.worktrees/${unit.branch.replace(/\//g, '-')}-validate` : unit.worktree + +const ciPrompt = (unit, mode, markFiles) => { + const tree = validationTree(unit) + return [ `Run the validation commands for the repository ${unit.repo}, branch ${unit.branch},`, - `in the worktree ${unit.worktree}. Run them in the reported order, sequentially — never in`, + `in the worktree ${tree}. Run them in the reported order, sequentially — never in`, `parallel.`, ``, - `First \`cd ${unit.worktree}\`, then run \`git branch --show-current\` and return its output`, - `as branch. Every command runs from there: each command's cwd in the report is relative to`, - `the repository root, so resolve it inside this worktree — never against ${unit.repo}, which`, - `is a different checkout on a different branch. If the branch you read is not ${unit.branch},`, - `run nothing: return it as branch with passed=false and say so in failures.`, + ...(tree === unit.worktree + ? [] + : [ + `That worktree is this branch's validation checkout, detached at its commit. Create it`, + `with \`git worktree add --detach ${tree} ${unit.branch}\` if it is not there; if it is,`, + `bring it to the branch's current commit with \`git -C ${tree} checkout --detach`, + `${unit.branch}\`. Never \`git clean\` it — installed dependencies live there untracked.`, + ``, + ]), + `\`cd ${tree}\` before anything else, and confirm what you are about to grade: \`git rev-parse`, + `HEAD\` there must equal \`git -C ${unit.repo} rev-parse ${unit.branch}\`. When they match,`, + `return branch "${unit.branch}". When they do not, run nothing: return the branch you actually`, + `found (or the short HEAD sha when detached) as branch, with passed=false and the mismatch in`, + `failures. Every command runs from that worktree: each command's cwd in the report is relative`, + `to the repository root, so resolve it there — never against ${unit.repo}, which is a`, + `different checkout on a different branch.`, ``, `Toolchain report for this repository:`, ``, @@ -318,12 +336,14 @@ const ciPrompt = (unit, mode, markFiles) => `\`status: done\` in the frontmatter of these task files, changing nothing else in them,`, `and return marked=true:`, ...unit.taskFiles.map((f) => `- ${f}`), - `They are the run's state store: they live outside the repository, they are never`, - `committed, and the no-fixing rule above is about the code, not about them. On any`, - `failure leave them untouched and return marked=false.`, + `They are the run's state store, and the no-fixing rule above is about the code, not`, + `about them: edit them at the absolute paths listed, commit nothing, and if they happen`, + `to sit inside a checkout of this repository, leave that checkout's other files alone.`, + `On any failure leave them untouched and return marked=false.`, ] : []), ].join('\n') +} const FIX_RESULT = { type: 'object', @@ -377,11 +397,12 @@ const ciFault = (ci, unit) => { // The final gate flips this branch's task files to done itself, so their own dirtiness is // expected wherever the tasks directory happens to live; anything else is the runner's edit. const own = new Set(unit.taskFiles || []) + const tree = validationTree(unit) const stray = (ci.dirty || '') .split('\n') .filter((line) => line.trim()) .map(porcelainPath) - .filter((p) => p && !own.has(`${unit.worktree}/${p}`)) + .filter((p) => p && !own.has(`${tree}/${p}`)) if (stray.length > 0) { const shown = stray.slice(0, 5).join(', ') return `left ${stray.length} uncommitted change(s) in the worktree (${shown}${stray.length > 5 ? ', …' : ''}), so its verdict describes a tree no commit holds` From 2c89a3f15128aacd752966beb6731d3d88327d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:13:06 +0200 Subject: [PATCH 06/44] fix(fd3): run a review fan-out the workflow can actually perform All five review agents of one run invoked start-cr inside a workflow step, where no Agent tool exists; three degraded to a single inline pass without saying so. --- plugins/code-review/CHANGELOG.md | 3 +++ plugins/code-review/commands/start-cr.md | 7 +++++++ .../__pycache__/dispatch.cpython-312.pyc | Bin 0 -> 35739 bytes .../__pycache__/dispatch.cpython-313.pyc | Bin 0 -> 35454 bytes .../__pycache__/pre_llm_pass.cpython-312.pyc | Bin 0 -> 24666 bytes .../__pycache__/pre_llm_pass.cpython-313.pyc | Bin 0 -> 25358 bytes .../test_dispatch.cpython-312-pytest-9.0.3.pyc | Bin 0 -> 49508 bytes .../__pycache__/test_dispatch.cpython-313.pyc | Bin 0 -> 44906 bytes ...st_pre_llm_pass.cpython-312-pytest-9.0.3.pyc | Bin 0 -> 19819 bytes .../test_pre_llm_pass.cpython-313.pyc | Bin 0 -> 19769 bytes plugins/fd3/CHANGELOG.md | 3 +++ plugins/fd3/skills/implement-tasks/SKILL.md | 13 ++++++++++--- 12 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 plugins/code-review/scripts/__pycache__/dispatch.cpython-312.pyc create mode 100644 plugins/code-review/scripts/__pycache__/dispatch.cpython-313.pyc create mode 100644 plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-312.pyc create mode 100644 plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-313.pyc create mode 100644 plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-312-pytest-9.0.3.pyc create mode 100644 plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-313.pyc create mode 100644 plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-312-pytest-9.0.3.pyc create mode 100644 plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-313.pyc diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 38a3bf6..861acd0 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the Lens is re-dispatched - The unavailable-lens question offers exactly proceed-without-it or abort — reviewing that lens inline is not an option on it +- Step 3 states what to do where the `Agent` tool is absent (inside another agent or a workflow + step): say so up front and hand the caller the choice, never discover it halfway and report a + single pass as an eight-lens review - Scanner search is described tool-neutrally (`Grep`, or `git grep` where sub-agents have no `Grep` tool) - Step 4 now judges the fix as well as the finding — behaviour preserved, no contradiction with diff --git a/plugins/code-review/commands/start-cr.md b/plugins/code-review/commands/start-cr.md index a629bac..12a84f2 100644 --- a/plugins/code-review/commands/start-cr.md +++ b/plugins/code-review/commands/start-cr.md @@ -158,6 +158,13 @@ return inline at once, so the harness backgrounds them — this holds **even if `run_in_background: false`**, because the flag cannot make a concurrent fan-out synchronous. Let them background; that is the working path. +**Without the `Agent` tool there is no review to run.** In some contexts — inside another +agent, inside a workflow step — it is simply absent, and a single pass by one reader is not this +command however carefully it reads. Say so in your first sentence, name the lenses that will not +run, and let the caller decide between an announced single-pass reading and invoking +`/quality-review`, `/comment-review` and `/security-review` as their own agents. Never discover +this silently halfway and report the result as a review. + **Never pass `name:` to a Scanner call.** Naming routes the Scanner into the agent-teams mailbox, where its findings come back only if you ask for them and it answers — a channel that has failed outright in practice, leaving an orchestrator with every Scanner signalling diff --git a/plugins/code-review/scripts/__pycache__/dispatch.cpython-312.pyc b/plugins/code-review/scripts/__pycache__/dispatch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..678c333736872312e4cde32ee241dcc1fad5701e GIT binary patch literal 35739 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

mHob_Xag=BEn-z;lbeBShlSH~lUVkJS`V8Onb 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 literal 0 HcmV?d00001 diff --git a/plugins/code-review/scripts/__pycache__/dispatch.cpython-313.pyc b/plugins/code-review/scripts/__pycache__/dispatch.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17671d42dd7964de3dffb732dfb3cfebcf87f5fe GIT binary patch literal 35454 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 literal 0 HcmV?d00001 diff --git a/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-312.pyc b/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7de1bdbd63af2c43198051cead1cdbcab773eb91 GIT binary patch literal 24666 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 literal 0 HcmV?d00001 diff --git a/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-313.pyc b/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc06fd01430986d9fa27d065e0d1a49d46344687 GIT binary patch literal 25358 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 BPj&zR literal 0 HcmV?d00001 diff --git a/plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-313.pyc b/plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e671bdfd5852fe7cff7d909dad454deb3ce6602 GIT binary patch literal 44906 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 Oi)SF$IG3{}&Hn+qV?Kod literal 0 HcmV?d00001 diff --git a/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-312-pytest-9.0.3.pyc b/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-312-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..948ebd318fd73694836eb7915ea5bbb0b4f2e757 GIT binary patch literal 19819 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 literal 0 HcmV?d00001 diff --git a/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-313.pyc b/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b235f1014b8acfc951a8d26a13fcaf48a2330394 GIT binary patch literal 19769 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 literal 0 HcmV?d00001 diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index e073120..f756986 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 what actually holds: edit them at their absolute paths, commit nothing, touch nothing else - `implement-tasks` says that a pre-launch commit of the spec and tasks directory must carry the repository's regenerated indexes, or say it did not — a stale one fails every branch at once +- `implement-tasks` step 2 offers only review skills that review inline, and expands a fan-out + orchestrator the user names (`code-review:start-cr`) into its single-lens skills — inside a + workflow agent there is no `Agent` tool, so the orchestrator silently degraded to one pass ## [0.1.0] - 2026-09-04 diff --git a/plugins/fd3/skills/implement-tasks/SKILL.md b/plugins/fd3/skills/implement-tasks/SKILL.md index 98a11b7..9c1fd2e 100644 --- a/plugins/fd3/skills/implement-tasks/SKILL.md +++ b/plugins/fd3/skills/implement-tasks/SKILL.md @@ -110,9 +110,16 @@ Then make the graph launchable: One batch, following `${CLAUDE_SKILL_DIR}/../../references/question-batching.md`: - which code-review skills to run during validation — offer only names present in this session's - skill listing, never one recalled from memory; the lens is roughly two fifths of the run, and a - review bot on the pull request finds different things, not the same ones — `none` is a valid - answer but a real trade; + skill listing, never one recalled from memory; the lens costs roughly a quarter to two fifths + of the run, and a review bot on the pull request finds different things, not the same ones — + `none` is a valid answer but a real trade. **Offer only skills that review inline.** A review + agent in the workflow has no `Agent` tool, so a skill or command that fans out into scanners of + its own — `code-review:start-cr` is the one to watch for — cannot do what its name promises + there: it quietly reviews everything itself in one pass, which is the single perspective the + fan-out exists to avoid. When the user names one anyway, expand it into the single-lens skills + it orchestrates (for `start-cr`: `code-review:quality-review`, `code-review:comment-review`, + `code-review:security-review`), pass those as `reviewSkills`, and say that is what you did — + each becomes its own review agent, which is the fan-out the workflow can actually run; - the spec path, when the `spec:` pointers did not resolve to an existing file in step 1; - any unresolved repository paths, `branch-base:` disagreements and stale `in-progress` calls from step 1; From 413102df906ad73a659366d9ad140d195f6c7b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:15:52 +0200 Subject: [PATCH 07/44] feat(code-review): apply-phase discipline, and rules for IaC exposure and access widening Runs rewrote files with sed and formatted whole repositories, lost their approved list to a compaction mid-walk, and had no rule for a private key sitting in prod state or for a contract permission quietly widened to org-wide read. --- plugins/code-review/CHANGELOG.md | 19 ++++++ plugins/code-review/CONTEXT.md | 2 +- plugins/code-review/commands/start-cr.md | 35 +++++++++-- .../adr/0002-active-lens-set-and-standards.md | 6 +- .../code-review/references/rules/security.md | 62 ++++++++++++++++++- plugins/code-review/references/severity.md | 10 ++- .../skills/quality-review/SKILL.md | 2 +- 7 files changed, 122 insertions(+), 14 deletions(-) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 861acd0..300472c 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -36,6 +36,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `Reconciliation` line gained a `P primary dropped` term and each of its counts now names the rendered block it is checked against (`C` against `Boy-scout`, `D + P` against `Not flagged`); `Not flagged` entries stay countable so the check can be verified from the report +- Apply-phase discipline: edits go through the `Edit` tool (no `sed`/heredoc rewrites), a + formatter runs only on the files the review edited, an approved fix that cannot be applied as + approved goes back to the user instead of being substituted, and the wrap-up lists every fix + skipped, substituted or extended +- The rendered report and the user's selection are written to the session scratchpad before the + apply walk, so a compaction mid-walk no longer costs the approved list +- A confirmed exposure that no rule names still leads the report from its own `Not flagged` + bullet + +### Added + +- `security` · **`iac-exposure`** (high) — infrastructure code that materializes a secret into + state or an unmarked output, or grants trust wider than the identity it names (an OIDC + condition matching beyond the intended workflow, a wildcard principal, anonymous access) +- `security` · **`access-widening`** (high) — a change that relaxes an authorization boundary + that existed: a weaker permission, a removed guard, a dropped owner predicate, a widened + allowlist +- `missing-access-check` calibration now routes a test that would stay green if the guard + regressed to `tests` · test-fidelity, instead of grading a test gap as a security high ## [0.3.0] - 2026-09-02 diff --git a/plugins/code-review/CONTEXT.md b/plugins/code-review/CONTEXT.md index 128232d..7c175c8 100644 --- a/plugins/code-review/CONTEXT.md +++ b/plugins/code-review/CONTEXT.md @@ -25,7 +25,7 @@ The single source of truth for one Lens's rule text: `references/rules/.md One of the eleven stable top-level labels in the quality vocabulary: `readability`, `tests`, `naming`, `module`, `objects`, `patterns`, `simplicity`, `security`, `performance`, `spec`, and `standards`. Ten are fixed by the plugin; `standards` is repo-defined (its rules come from the Standards file). **Rule**: -A specific sub-tag under a Family — one of the 42 fixed rules across the ten plugin-defined Families, or a repo-defined `standards` rule whose slug derives from the quoted rule — or one of the comment rules `R1`–`R12`. +A specific sub-tag under a Family — one of the 44 fixed rules across the ten plugin-defined Families, or a repo-defined `standards` rule whose slug derives from the quoted rule — or one of the comment rules `R1`–`R12`. **Finding**: The quality-side unit of output: `family` · rule · severity · lines → fix. diff --git a/plugins/code-review/commands/start-cr.md b/plugins/code-review/commands/start-cr.md index 12a84f2..bb9f84b 100644 --- a/plugins/code-review/commands/start-cr.md +++ b/plugins/code-review/commands/start-cr.md @@ -468,7 +468,7 @@ One terse line each. Omit a block when it is empty. resolved under its own name. - **Re-grade every quality finding's severity yourself** against the master table in `${CLAUDE_PLUGIN_ROOT}/references/severity.md` — read it now if you have not. It - carries the 42 rows, what each severity means, the anti-anchoring rule, and the + carries the 44 rows, what each severity means, the anti-anchoring rule, and the **`standards` keyword mapping** (MUST / MUST NOT / NEVER / ALWAYS → high, SHOULD → medium, MAY / prefer / consider → nit, no keyword → medium). A `standards` finding has no fixed row: re-grade it against that mapping by re-reading the rule it quotes, not @@ -606,7 +606,9 @@ Rules for filling it in: `high` or `medium` finding, **or** any comment REMOVE / REWRITE / MOVE / ADD, the headline names the worst one — it must not call the change "clean", "well-structured", or "only cosmetic nits". A confirmed **`security`** finding is the - headline over any craft finding, whatever their severities; a `spec` · + headline over any craft finding, whatever their severities — and so is a confirmed + **exposure that no rule names**, which leads the report from its own `Not flagged` + bullet rather than being demoted for want of a tag; a `spec` · missing-requirement or wrong-implementation forbids the clean headline outright. Reserve the clean verdict for a tally that is genuinely nits-only-and-all-KEEP (or empty). @@ -711,12 +713,35 @@ must stay honest when findings don't spread across them: - A before/after **preview** diff belongs in an `AskUserQuestion` option, never in the report body — Step 5 stays clause-only. +**Put the review on disk before the apply phase starts.** The apply walk is the longest stretch +of the run and the one most likely to be compacted; when that happens mid-walk, the report and +the user's answer are gone, and a run that had to reconstruct its approved list by parsing its +own transcript spent that effort for nothing. Write the rendered report to a file in the session +scratchpad before the menu, and the user's selection — each approved finding with its file, site +and exact fix — under it as soon as the answer arrives. Read it back rather than recalling it, +and say where it is in the wrap-up. + Apply with `Edit` only what the user selects; **auto-apply nothing structural without an explicit yes**. Only findings confirmed in Step 4 enter an apply batch. -**`Write` creates a file that does not exist yet, and nothing else.** The one case is -a new file the user picked from the menu — the missing spec a correctness bucket -offered, say. Every change to a file already on disk goes through `Edit`, so a +**`Edit` means the tool, not "an edit".** No `sed -i`, no Python or heredoc rewrite, no `awk`, +however convenient the shell looks for a repeated change: `Edit` fails loudly when the text it +expects is not there, and a shell rewrite silently hits every look-alike in the file — one run's +blanket strip took out the project's own documented comment prefix, which its conventions note +had just said to leave alone. A formatter runs on the files you edited, never across the package +or the repository: three runs reflowed snapshots, fixtures and a protected `tsconfig` that way, +then had to revert them and explain them to the user as "not mine". + +**An approved fix that cannot be applied as approved goes back to the user.** A hook blocks it, +the site turns out ambiguous, the edit needs a companion change nobody approved — say which fix, +what stopped it, and what you would do instead; never substitute a different edit (one run +deleted a test where the approved fix was to fold it into another) and mention it in passing +afterwards. The wrap-up lists every approved fix that was skipped, substituted or extended, with +its reason, and claims nothing the tree does not carry. + +**`Write` creates a file that does not exist yet, and nothing else.** Two cases: a new file +the user picked from the menu — the missing spec a correctness bucket offered, say — and the +review's own scratchpad file above, which lives outside the repository. Every change to a file already on disk goes through `Edit`, so a targeted fix can never turn into a wholesale rewrite of a file the review only read in part. This is the Orchestrator's alone: a Scanner still writes nothing at all. diff --git a/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md b/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md index 73faaea..758ef1a 100644 --- a/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md +++ b/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md @@ -67,8 +67,10 @@ keeps its suppress-only role. ## Consequences - "Five" is no longer an invariant anywhere: every count in the command, the - skills, the references, and the docs is N, 8, 11, or 42, and a future lens - adds a gate to Step 2b rather than a new number to hunt down. + skills, the references, and the docs is N, 8, 11, or the master table's row + count, and a future lens adds a gate to Step 2b rather than a new number to hunt + down. The row count was 42 when this decision was taken; run analysis has since + added `security` · `iac-exposure` and `access-widening`, making it 44. - The gated lenses have no eval surface except the scanner-track prompts — no standalone skill means no skill-level fixture, so their recall and noise gates emulate the brief directly. diff --git a/plugins/code-review/references/rules/security.md b/plugins/code-review/references/rules/security.md index 7f128ac..2e0e3ad 100644 --- a/plugins/code-review/references/rules/security.md +++ b/plugins/code-review/references/rules/security.md @@ -25,7 +25,7 @@ first pass. ## Contents - `security` — secret-in-source, injection-sink, missing-access-check, - unvalidated-boundary, insecure-setting + unvalidated-boundary, insecure-setting, iac-exposure, access-widening Every rule below carries its **Flag** conditions, a **Suggested fix**, and a **Calibration** paragraph naming the look-alike that is *not* a violation. @@ -37,6 +37,8 @@ Every rule below carries its **Flag** conditions, a **Suggested fix**, and a | `security` | missing-access-check | handler reading/mutating a resource with no authn/authz guard, or request-supplied id with no ownership/tenant predicate | high | | `security` | unvalidated-boundary | HTTP/CLI/env/queue/third-party payload used in logic or persistence with no parse/validate at entry | medium | | `security` | insecure-setting | a literal disabling a protection (`rejectUnauthorized:false`, `verify=False`, unsafe `yaml.load`, `Math.random` for tokens, CORS `*`+credentials) | high | +| `security` | iac-exposure | infrastructure code storing a secret where others can read it, or admitting an identity wider than the one it names | high | +| `security` | access-widening | the change relaxes an authorization boundary that existed — a weaker permission, a dropped guard or owner predicate, an allowlist opened up | high | ### Confirm the sink — the discipline for the whole lens @@ -146,7 +148,10 @@ Authentication says who is calling; authorization says whether *this* caller may registration (read it — an `app.use(auth)` above the route clears the whole group); a deliberately public endpoint (health, login, signup, a signature-verified webhook); a query already scoped to the session's own tenant one layer up; code with no - request-facing caller. This rule almost always needs the registration read; when it + request-facing caller — **a test that would stay green if the guard regressed is + `tests` · test-fidelity, not this rule**: the missing assertion is a test defect, and + grading it here turns a medium into a high and puts the whole review under a security + headline it has not earned. This rule almost always needs the registration read; when it is out of reach, the finding is `(verify)`, never an assertion. #### `unvalidated-boundary` — parse at the edge, then trust @@ -195,3 +200,56 @@ checks, safe parsing, unpredictable tokens, origin isolation. elsewhere — say so); `Math.random` for a non-security value (jitter, sampling); CORS `*` with no credentials on a public read-only API. A gate you cannot read is `(verify)`. + +#### `iac-exposure` — infrastructure that stores a secret readably, or trusts too widely + +Infrastructure code is in scope for this lens (`.tf`/HCL and the declarative surfaces +`scope.md` classifies as `iac`), and it fails differently from application code: nothing +is executed, so the harm sits in what a declaration *stores* and *admits*. + +- **Flag** when: + - a secret ends up somewhere the declaration does not control — a generated key or + password materialized as a resource attribute, so it lands in remote state + (`tls_private_key`, a service-account or access-key resource, a `local_file` of a + key); an output carrying a credential without `sensitive`; a plaintext `default` on + a credential variable; + - a trust or access grant is wider than the identity it names — a federation or OIDC + condition that matches beyond the branch, environment or workflow intended (a `sub` + pinned to `refs/heads/main` still matches a workflow that runs on + `pull_request_target`); a wildcard principal (`allUsers`, `AWS: "*"`, a project-wide + binding where one service account was meant); a bucket, topic or dataset opened to + anonymous access; `0.0.0.0/0` reaching a non-public port. + Ends: the declaration line and where the value becomes readable, or the identity the + grant admits — name it (`the prod state bucket`, `any workflow run of any fork`). +- **Suggested fix**: name the mechanism the stack already has — reference the secret + manager instead of materializing the value, mark the output `sensitive` and keep the + key out of state, tighten the condition to the full ref *and* the workflow, name the + exact principal, or replace the open CIDR with the peer range. +- **Calibration → not a finding**: a value read from a secret-manager data source; a + resource public by design (an assets bucket behind a CDN, a load balancer's public + address); a wildcard inside a scope the provider narrows by another condition you + have read; a fixture in a test or sandbox module; a breadth the Step 2 conventions + note documents. State cannot be read from the file, so a claim about *who* can read + the state bucket is `(verify)` unless the configuration in view says so. + +#### `access-widening` — the change relaxes a boundary that was there + +The diff is the evidence here: a permission, guard or predicate that stood on the `-` +side and is weaker or gone on the `+` side hands data to callers who could not reach it +yesterday, and nothing in the code looks wrong afterwards. + +- **Flag** when the change, on an existing path: swaps a required permission, role or + scope for a broader one (`admin:contract:read` → an org-wide read); removes or + loosens a guard, decorator or middleware; drops an owner or tenant predicate from a + query that had one; moves a route out of an authenticated group; widens an allowlist, + origin list or audience to a wildcard; lowers a validation that gated who may write. + Ends: the removed or weakened line (quote the `-` side) and the resource it now + admits. +- **Suggested fix**: name the boundary that was there and what would restore it, or the + narrower predicate that covers the new caller. +- **Calibration → not a finding**: a widening the `--spec` text or the Step 2 note + explicitly asks for (clear it in one prose line naming where it is written); a rename + of the same permission; a boundary moved rather than removed — the guard now sits one + layer up and you have read it; a new endpoint with no previous boundary, which is + `missing-access-check` territory if anything. When the diff does not show the previous + boundary, this rule does not apply. diff --git a/plugins/code-review/references/severity.md b/plugins/code-review/references/severity.md index 4d3f8b9..7440c5c 100644 --- a/plugins/code-review/references/severity.md +++ b/plugins/code-review/references/severity.md @@ -4,7 +4,7 @@ Every finding that reports as `family` · rule · severity carries one **family* **rule**, and one **severity**, all three **verbatim** from this table — never a code number, never a paraphrase invented this run. Two reviews of the same code name the same `family` · rule every time. Eleven families report in that shape: the ten below -with **42 fixed rows**, plus `standards`, whose rules are the project's own and are +with **44 fixed rows**, plus `standards`, whose rules are the project's own and are graded by the mapping at the end of this file. Severity is exactly one of `high`, `medium`, or `nit`. There is no `low`, no @@ -46,6 +46,8 @@ Severity is exactly one of `high`, `medium`, or `nit`. There is no `low`, no | `security` | missing-access-check | handler reading/mutating a resource with no authn/authz guard, or request-supplied id with no ownership/tenant predicate | high | | `security` | unvalidated-boundary | HTTP/CLI/env/queue/third-party payload used in logic or persistence with no parse/validate at entry | medium | | `security` | insecure-setting | a literal disabling a protection (`rejectUnauthorized:false`, `verify=False`, unsafe `yaml.load`, `Math.random` for tokens, CORS `*`+credentials) | high | +| `security` | iac-exposure | infrastructure code storing a secret where others can read it (state, an unmarked output), or a trust/access grant wider than the identity it names | high | +| `security` | access-widening | the change relaxes an authorization boundary that existed — a weaker permission, a removed guard or owner predicate, an allowlist opened up | high | | `performance` | n-plus-one | per-item DB/HTTP/IO call inside a loop over an unbounded collection where a batch form exists | high | | `performance` | unbounded-fetch | a list read with no limit/pagination over data that grows (incl. list endpoints) | medium | | `performance` | blocking-in-async | sync blocking call on a request-serving/event-loop path (N/A outside Node & Python asyncio) | medium | @@ -66,10 +68,12 @@ Severity is exactly one of `high`, `medium`, or `nit`. There is no `low`, no cycle or inverts the layering (`dependency-direction`), a helper the repo already exports (`canonical-helper`), a per-item call that multiplies with the data (`n-plus-one`), a spec line left unimplemented or implemented against its wording - (`missing-requirement`, `wrong-implementation`), and the four security rules that + (`missing-requirement`, `wrong-implementation`), and the six security rules that name a confirmed exposure — a literal credential (`secret-in-source`), untrusted data reaching a sink (`injection-sink`), an unguarded resource (`missing-access-check`), - and a protection switched off (`insecure-setting`). These cost the most to live with. + a protection switched off (`insecure-setting`), a secret or over-wide grant declared + into infrastructure (`iac-exposure`), and a boundary the change relaxes + (`access-widening`). These cost the most to live with. - **medium** — readability friction a reader feels every time, or a latent gap that matters: `ordering`, `test-structure` interleaving, a `test-fidelity` name/fixture that claims more than its assertions check, `guard-clause` nesting, an unexplained diff --git a/plugins/code-review/skills/quality-review/SKILL.md b/plugins/code-review/skills/quality-review/SKILL.md index 6ab41a6..0c2dd3c 100644 --- a/plugins/code-review/skills/quality-review/SKILL.md +++ b/plugins/code-review/skills/quality-review/SKILL.md @@ -226,7 +226,7 @@ anything: | `simplicity` | `${CLAUDE_PLUGIN_ROOT}/references/rules/simplicity-types.md` | **Severity comes from `${CLAUDE_PLUGIN_ROOT}/references/severity.md`** — the master -table of all 42 fixed rules, what `high` / `medium` / `nit` each mean, the keyword +table of all 44 fixed rules, what `high` / `medium` / `nit` each mean, the keyword mapping for `standards` findings, and the anti-anchoring rule. Read it and grade every finding against its own row there (a `standards` finding against its keyword). The family, the rule, and the severity are all used **verbatim**, so a reader (and a diff From f5278838a5de1a1885bd8d318cedacef89abf926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:19:07 +0200 Subject: [PATCH 08/44] feat(code-review): widen scope coverage and offer the spec lens the diff already carries Runs skipped .mjs build scripts as tooling, graded a tests-e2e/ suite as source, and left the spec lens off on a branch whose own SPEC.md was in the diff. --- plugins/code-review/CHANGELOG.md | 10 ++++++ plugins/code-review/commands/start-cr.md | 24 +++++++++---- plugins/code-review/references/scope.md | 43 +++++++++++++++++------- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 300472c..529af89 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -45,6 +45,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A confirmed exposure that no rule names still leads the report from its own `Not flagged` bullet +- Scope: `.mjs`/`.cjs`/`.mts`/`.cts` are reviewed as source, a directory whose name + contains `e2e` (or ends in `-tests`) classifies as `test`, `.txt` is skipped, and a CI + workflow file is skipped with a sentence naming its triggers, permissions and secret + handling as `/security-review` territory +- The "substance of the change" sentence now covers a docs, spec/ADR or release-notes + branch, not only a dependency manifest +- When no `--spec` was passed and the diff carries a spec-shaped file (`specs/`, + `docs/adr/`, `tasks/`, `*SPEC*.md`, …), `start-cr` offers to review the change against + it instead of silently leaving the `spec` lens off + ### Added - `security` · **`iac-exposure`** (high) — infrastructure code that materializes a secret into diff --git a/plugins/code-review/commands/start-cr.md b/plugins/code-review/commands/start-cr.md index bb9f84b..5df582f 100644 --- a/plugins/code-review/commands/start-cr.md +++ b/plugins/code-review/commands/start-cr.md @@ -5,7 +5,8 @@ description: >- security, performance, spec) in parallel over a change and merges them into one per-file report. Three lenses are gated by the input, never by the user: security is always on, performance runs only when executable source files are in scope, - spec only with `--spec `. Manual only — never auto-triggered. It resolves + spec only when a spec file is named — by `--spec `, or by the user accepting + the one the diff itself carries. Manual only — never auto-triggered. It resolves scope once, dispatches one scanner subagent per active lens, re-grades severity centrally, and offers a single apply menu. It never edits code during the review. allowed-tools: Read, Bash, Grep, Glob, Agent, AskUserQuestion, Edit, Write @@ -23,7 +24,7 @@ and only for what the user picks. This command is **explicit invocation only**; it is never auto-triggered. There is no lens selection — which Lenses run is decided by the input in Step 2b, never by user choice: the five craft Lenses and `security` always run, `performance` runs -when executable source is in scope, `spec` when `--spec` names a file. For a partial +when executable source is in scope, `spec` when a spec file is named. For a partial review the user invokes `/comment-review` or `/quality-review` directly. Arguments: `$ARGUMENTS` @@ -82,6 +83,17 @@ Scanner's `` is cut from this one list in Step 2b, and all of them get th offer to review uncommitted changes only or to pass `--base ` — **never guess silently**. +**When the change carries its own spec, offer the Lens.** If `--spec` was not passed +and the resolved list contains a specification-shaped file — a path under `specs/`, +`spec/`, `docs/adr/`, `tasks/`, or a name matching `*SPEC*.md`, `*ADR*.md`, `*.spec.md`, +`*-plan.md` — say so in one line and offer that path with a single `AskUserQuestion`: +review the change against it, or continue without the `spec` Lens. Offer the one file +that best fits (the most recently changed, or the one the other files sit under); more +than two options is a menu, not an offer. On acceptance, treat it exactly as a passed +`--spec` — Read it now — and note in the Tally that the spec Lens was activated from the +diff rather than from the flag. Do not make this offer twice, and never activate the +Lens without the user saying yes. + **Which files get judged** — the in-scope extensions, the skip list, and the rule about a skipped dependency manifest that is the substance of the change — is in `${CLAUDE_PLUGIN_ROOT}/references/scope.md`. Read it and apply it to the resolved @@ -139,13 +151,13 @@ never from a preference: - **`performance`** is active iff the `source`-kind subset of the resolved list, **minus `.sh` files**, is non-empty — a tests-only, IaC-only, or shell-only change skips it; -- **`spec`** is active iff `--spec` was given and resolved to a readable local file in - Step 1. +- **`spec`** is active iff a spec resolved to a readable local file in Step 1 — from + `--spec`, or from the offer the user accepted when the change carried its own spec. Record **N**, the number of active Lenses, and for each one its own ``: `performance` gets the source subset it was gated on; every other Lens gets the full resolved list. Record every **inactive** Lens with its reason (`performance — no -executable code`, `spec — no --spec`); the Tally prints them in Step 5. From here on +executable code`, `spec — no spec named`); the Tally prints them in Step 5. From here on **N** means this count: N Scanners dispatched, N `` blocks awaited, N outputs merged. @@ -614,7 +626,7 @@ Rules for filling it in: empty). - **The `Tally` names the lenses.** `Lenses: L of 8` always, with each skipped Lens and its Step 2b reason in the parenthesis (`skipped: performance — no executable - code; spec — no --spec`); drop the parenthesis when all eight ran. When a spec was + code; spec — no spec named`); drop the parenthesis when all eight ran. When a spec was given, add the `spec` Scanner's met-requirements count as `Spec: R of T requirements met`; omit that clause otherwise. diff --git a/plugins/code-review/references/scope.md b/plugins/code-review/references/scope.md index 25c7d3c..7d536b2 100644 --- a/plugins/code-review/references/scope.md +++ b/plugins/code-review/references/scope.md @@ -9,29 +9,44 @@ findings. ## In scope -Source files that carry human-authored code and comments: `.ts .tsx .js .jsx .py .go -.rs .java .kt .swift .c .cpp .h .rb .php .vue .scala .cs .sh`, plus +Source files that carry human-authored code and comments: `.ts .tsx .mts .cts .js .jsx +.mjs .cjs .py .go .rs .java .kt .swift .c .cpp .h .rb .php .vue .scala .cs .sh`, plus **infrastructure-as-code** (`.tf`/HCL and similar declarative surfaces that still carry -comments and structure worth reviewing). +comments and structure worth reviewing). The ES-module and CommonJS extensions carry the +same hand-written code as `.js` — a build script or a config-as-code module under +`.mjs`/`.cjs` is reviewed, not skipped as tooling. ## Skip JSON, lockfiles, generated or minified files (a generator's `.d.ts`, `*_pb.*`, anything under `dist/`, `build/`, `node_modules/`), `.md` and docs (in a comment review the prose -*is* the content), **static config data** (`.yaml`/`.toml`/`.ini` settings, `.env`), and -license/SPDX headers. +*is* the content), plain text (`.txt`), **static config data** (`.yaml`/`.toml`/`.ini` +settings, `.env`), and license/SPDX headers. Note every skipped file in one line, so coverage stays honest. +**CI workflow definitions are skipped with a security sentence.** A changed +`.github/workflows/*.yml`, `.gitlab-ci.yml`, or equivalent pipeline file is static config +by these rules, but it is the one skipped kind that routinely carries a real exposure — a +`pull_request_target` job checking out and running untrusted code, a secret passed into a +step that echoes it, a third-party action pinned to a moving tag. Say on the `Skipped` +line that the pipeline files were not line-graded and that their permissions, triggers and +secret handling belong to `/security-review`. If the `security` Scanner nonetheless reads +one and finds a confirmed exposure, that finding stands — `security` · `iac-exposure` +covers it. + **A skipped file that is the substance of the change gets its own sentence.** A dependency manifest (`package.json`, `composer.json`, …) on a dependency-bump or upgrade -branch is the whole point of that diff. Say so explicitly rather than burying it in the -skip list: its dependency changes aren't line-graded, and the reader should read that as -a deliberate scope boundary rather than an oversight. The same sentence carries a second -boundary: a changed `.env*`, dependency manifest, or lockfile is also **not secret- or -dependency-scanned** by the `security` lens, which reads source files only. Say that on -the `Skipped` line and point the reader to `/security-review` for the dependency and -configuration audit this review does not do. +branch is the whole point of that diff, and so is the prose on a documentation branch, a +spec/ADR change, or a release-notes commit — when the skipped files *are* the change, +name that in a sentence of its own and say what the review therefore covers (often only +the handful of source files that came along for the ride). Say so explicitly rather than +burying it in the skip list: the skipped content isn't line-graded, and the reader should +read that as a deliberate scope boundary rather than an oversight. The same sentence +carries a second boundary: a changed `.env*`, dependency manifest, or lockfile is also +**not secret- or dependency-scanned** by the `security` lens, which reads source files +only. Say that on the `Skipped` line and point the reader to `/security-review` for the +dependency and configuration audit this review does not do. ## File kinds @@ -40,7 +55,9 @@ eyeballing the content: - **`test`** — any file under one of these directories: `__tests__/`, `test/`, `tests/`, `spec/`, `e2e/`, `cypress/`, `fixtures/`, `__mocks__/`, `__snapshots__/`, - `testdata/`; or whose name matches one of: `*.test.*`, `*.spec.*`, `*_test.go`, + `testdata/`; under any directory whose name *contains* `e2e` (`tests-e2e/`, + `e2e-tests/`, `apps/web-e2e/`) or ends in `-tests`/`-test`; or whose name matches one + of: `*.test.*`, `*.spec.*`, `*_test.go`, `test_*.py`, `*_test.py`, `conftest.py`, `*Test.php`, `*Test.java`, `*Test.kt`, `*Tests.cs`, `*_spec.rb`, `*.feature`, `*.stories.*`, `setupTests.*`. - **`iac`** — `.tf`/HCL and the other declarative infrastructure-as-code surfaces named From e4ba78dd061dab6f130608f7310cee5d92869aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:20:41 +0200 Subject: [PATCH 09/44] fix(code-review): do not call a pushed branch empty because its upstream is itself git tracks a pushed feature branch against origin/feature, so the committed diff is empty and a review of the whole branch reported nothing to review. --- plugins/code-review/CHANGELOG.md | 4 + plugins/code-review/commands/start-cr.md | 8 +- plugins/code-review/scripts/get_changes.py | 81 +++++++++++----- ...t_get_changes.cpython-314-pytest-9.1.1.pyc | Bin 0 -> 9991 bytes .../scripts/tests/test_get_changes.py | 87 ++++++++++++++++++ .../skills/comment-review/SKILL.md | 8 +- .../skills/quality-review/SKILL.md | 8 +- 7 files changed, 170 insertions(+), 26 deletions(-) create mode 100644 plugins/code-review/scripts/tests/__pycache__/test_get_changes.cpython-314-pytest-9.1.1.pyc create mode 100644 plugins/code-review/scripts/tests/test_get_changes.py diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 529af89..1da4a57 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -45,6 +45,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A confirmed exposure that no rule names still leads the report from its own `Not flagged` bullet +- `get_changes.py` reports an `alternate` base when the resolved one saw no committed + change and another (`origin/main`, …) holds commits — a branch pushed to its own remote + counterpart no longer reads as "nothing to review"; `start-cr`, `comment-review` and + `quality-review` offer the re-run instead of stopping - Scope: `.mjs`/`.cjs`/`.mts`/`.cts` are reviewed as source, a directory whose name contains `e2e` (or ends in `-tests`) classifies as `test`, `.txt` is skipped, and a CI workflow file is skipped with a sentence naming its triggers, permissions and secret diff --git a/plugins/code-review/commands/start-cr.md b/plugins/code-review/commands/start-cr.md index 5df582f..e23d764 100644 --- a/plugins/code-review/commands/start-cr.md +++ b/plugins/code-review/commands/start-cr.md @@ -60,7 +60,13 @@ Scanner's `` is cut from this one list in Step 2b, and all of them get th (append `--base ` to both when the user passed one.) Read the `count` of each: - - both zero → tell the user there is nothing to review and **stop**; + - both zero → before concluding, look for an `alternate` object in the `committed` + output: the script adds it when the resolved base saw nothing but another base + (usually `origin/main`) holds real commits, which is what a freshly pushed branch + tracking its own remote counterpart looks like. When it is there, say which base was + used, which one differs and by how many files, and offer to re-run with + `--base ` — do not report "nothing to review" over it. With no + `alternate`, tell the user there is nothing to review and **stop**; - exactly one non-zero → use that scope automatically; - both non-zero → ask with **one** `AskUserQuestion` which to review — **Uncommitted** (working tree vs HEAD), **Committed** (HEAD vs base), or diff --git a/plugins/code-review/scripts/get_changes.py b/plugins/code-review/scripts/get_changes.py index d6c16f0..26ba215 100755 --- a/plugins/code-review/scripts/get_changes.py +++ b/plugins/code-review/scripts/get_changes.py @@ -23,7 +23,13 @@ "files": [ {"path": "src/foo.ts", "status": "M", "binary": false} ], - "count": 1 + "count": 1, + "alternate": { // only when the run found nothing + "ref": "origin/main", // and another base would have + "base": "def5678", + "diff_args": ["def5678..HEAD"], + "count": 7 + } } Status codes follow `git diff --name-status`: @@ -58,25 +64,52 @@ def _ref_exists(ref: str) -> bool: return res.returncode == 0 -def _resolve_base(explicit: Optional[str]) -> str: +BASE_CANDIDATES = ["@{upstream}", "origin/main", "origin/master", "main", "master"] + + +def _merge_base(ref: str) -> Optional[str]: + if not _ref_exists(ref): + return None + return _run(["git", "merge-base", "HEAD", ref], check=False).strip() or None + + +def _resolve_base(explicit: Optional[str]) -> tuple[str, str]: if explicit: if not _ref_exists(explicit): raise SystemExit(f"--base ref does not exist: {explicit}") merge_base = _run(["git", "merge-base", "HEAD", explicit]).strip() - return merge_base or explicit - - candidates = ["@{upstream}", "origin/main", "origin/master", "main", "master"] - for ref in candidates: - if _ref_exists(ref): - mb = _run(["git", "merge-base", "HEAD", ref], check=False).strip() - if mb: - return mb + return merge_base or explicit, explicit + + for ref in BASE_CANDIDATES: + mb = _merge_base(ref) + if mb: + return mb, ref raise SystemExit( "could not resolve a base ref — set upstream, push to origin/main, " "or pass --base " ) +def _alternate(scope: str, base: str, used_ref: str) -> Optional[dict]: + """A second base worth reporting when the resolved one saw no change. + + A branch whose upstream is its own remote counterpart diffs to nothing the + moment it is pushed, which reads as "no changes" while the whole branch is + still unreviewed against the trunk. + """ + for ref in BASE_CANDIDATES[1:]: + if ref == used_ref: + continue + mb = _merge_base(ref) + if not mb or mb == base: + continue + ref_args = [mb] if scope == "both" else [f"{mb}..HEAD"] + files = _list_files(ref_args) + if files: + return {"ref": ref, "base": mb, "diff_args": ref_args, "count": len(files)} + return None + + def _is_binary(path: str, ref_args: list[str]) -> bool: """Detect binary files via git diff --numstat ('-' for binary).""" out = _run(["git", "diff", "--numstat", *ref_args, "--", path], check=False) @@ -153,15 +186,16 @@ def main() -> int: raise SystemExit("not inside a git repository") include_untracked = False + used_ref = None if args.scope == "uncommitted": ref_args = ["HEAD"] base = None include_untracked = True elif args.scope == "committed": - base = _resolve_base(args.base) + base, used_ref = _resolve_base(args.base) ref_args = [f"{base}..HEAD"] else: # both - base = _resolve_base(args.base) + base, used_ref = _resolve_base(args.base) ref_args = [base] include_untracked = True @@ -171,17 +205,18 @@ def main() -> int: for u in _list_untracked(): if u["path"] not in tracked_paths: files.append(u) - json.dump( - { - "scope": args.scope, - "base": base, - "diff_args": ref_args, - "files": files, - "count": len(files), - }, - sys.stdout, - indent=2, - ) + payload = { + "scope": args.scope, + "base": base, + "diff_args": ref_args, + "files": files, + "count": len(files), + } + if not files and base and not args.base: + alternate = _alternate(args.scope, base, used_ref) + if alternate: + payload["alternate"] = alternate + json.dump(payload, sys.stdout, indent=2) sys.stdout.write("\n") return 0 diff --git a/plugins/code-review/scripts/tests/__pycache__/test_get_changes.cpython-314-pytest-9.1.1.pyc b/plugins/code-review/scripts/tests/__pycache__/test_get_changes.cpython-314-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1333d8a6a9aff8cc74a8369cde4f666188fbb538 GIT binary patch literal 9991 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 literal 0 HcmV?d00001 diff --git a/plugins/code-review/scripts/tests/test_get_changes.py b/plugins/code-review/scripts/tests/test_get_changes.py new file mode 100644 index 0000000..657d947 --- /dev/null +++ b/plugins/code-review/scripts/tests/test_get_changes.py @@ -0,0 +1,87 @@ +"""get_changes.py against real git repositories built per test.""" +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).resolve().parents[1] / "get_changes.py" + + +def git(repo: Path, *args: str) -> str: + res = subprocess.run( + ["git", *args], cwd=repo, capture_output=True, text=True, check=True + ) + return res.stdout + + +def commit(repo: Path, name: str, body: str) -> None: + (repo / name).write_text(body) + git(repo, "add", name) + git(repo, "commit", "-m", f"add {name}") + + +def run_script(repo: Path, *args: str) -> dict: + res = subprocess.run( + [sys.executable, str(SCRIPT), *args], + cwd=repo, capture_output=True, text=True, check=True, + ) + return json.loads(res.stdout) + + +@pytest.fixture +def origin_repo(tmp_path: Path) -> Path: + """A clone whose branch tracks its own pushed counterpart on origin.""" + upstream = tmp_path / "origin.git" + seed = tmp_path / "seed" + seed.mkdir() + git(seed, "init", "-b", "main") + git(seed, "config", "user.email", "t@example.com") + git(seed, "config", "user.name", "T") + commit(seed, "base.ts", "export const a = 1\n") + git(seed, "clone", "--bare", str(seed), str(upstream)) + + work = tmp_path / "work" + subprocess.run(["git", "clone", str(upstream), str(work)], check=True, + capture_output=True) + git(work, "config", "user.email", "t@example.com") + git(work, "config", "user.name", "T") + git(work, "checkout", "-b", "feature") + commit(work, "feature.ts", "export const b = 2\n") + git(work, "push", "-u", "origin", "feature") + return work + + +def test_committed_reports_alternate_base_when_upstream_sees_nothing(origin_repo: Path): + out = run_script(origin_repo, "--scope", "committed") + + assert out["count"] == 0 + alt = out["alternate"] + assert alt["ref"] == "origin/main" + assert alt["count"] == 1 + files = run_script(origin_repo, "--scope", "committed", "--base", alt["ref"])["files"] + assert [f["path"] for f in files] == ["feature.ts"] + + +def test_no_alternate_when_the_resolved_base_already_sees_the_change(origin_repo: Path): + git(origin_repo, "commit", "--allow-empty", "-m", "unpushed") + + out = run_script(origin_repo, "--scope", "committed") + + assert out["count"] == 0 # the empty commit touches no file + assert "alternate" in out # …but origin/main still holds feature.ts + + commit(origin_repo, "later.ts", "export const c = 3\n") + out = run_script(origin_repo, "--scope", "committed") + assert [f["path"] for f in out["files"]] == ["later.ts"] + assert "alternate" not in out + + +def test_explicit_base_never_gets_an_alternate(origin_repo: Path): + out = run_script(origin_repo, "--scope", "committed", "--base", "HEAD") + + assert out["count"] == 0 + assert "alternate" not in out diff --git a/plugins/code-review/skills/comment-review/SKILL.md b/plugins/code-review/skills/comment-review/SKILL.md index 8bdffdc..6c270e1 100644 --- a/plugins/code-review/skills/comment-review/SKILL.md +++ b/plugins/code-review/skills/comment-review/SKILL.md @@ -94,7 +94,13 @@ Parse the invocation arguments: (append `--base ` to both when the user passed one.) Read the `count` of each: - - both zero → tell the user there is nothing to review and stop; + - both zero → before concluding, look for an `alternate` object in the `committed` + output: the script adds it when the resolved base saw nothing but another base + (usually `origin/main`) holds real commits, which is what a freshly pushed branch + tracking its own remote counterpart looks like. When it is there, say which base was + used, which one differs and by how many files, and offer to re-run with + `--base ` — do not report "nothing to review" over it. With no + `alternate`, tell the user there is nothing to review and stop; - exactly one non-zero → use that scope automatically; - both non-zero → ask with `AskUserQuestion` which to review — **Uncommitted** (working tree vs HEAD), **Committed** (HEAD vs base), or **Both** (base → working diff --git a/plugins/code-review/skills/quality-review/SKILL.md b/plugins/code-review/skills/quality-review/SKILL.md index 0c2dd3c..388a351 100644 --- a/plugins/code-review/skills/quality-review/SKILL.md +++ b/plugins/code-review/skills/quality-review/SKILL.md @@ -113,7 +113,13 @@ Parse the invocation arguments: (append `--base ` to both when the user passed one.) Read the `count` of each: - - both zero → tell the user there is nothing to review and stop; + - both zero → before concluding, look for an `alternate` object in the `committed` + output: the script adds it when the resolved base saw nothing but another base + (usually `origin/main`) holds real commits, which is what a freshly pushed branch + tracking its own remote counterpart looks like. When it is there, say which base was + used, which one differs and by how many files, and offer to re-run with + `--base ` — do not report "nothing to review" over it. With no + `alternate`, tell the user there is nothing to review and stop; - exactly one non-zero → use that scope automatically; - both non-zero → ask with `AskUserQuestion` which to review — **Uncommitted** (working tree vs HEAD), **Committed** (HEAD vs base), or **Both** (base → working From 63ec60d28c0aef51cb1563611ebdd1cb012746cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:22:18 +0200 Subject: [PATCH 10/44] feat(fd3): split declared gaps, protected paths, and same-branch edges that name what they share A run stopped on a blocked claim the spec had already assigned an owner, bundled a CODEOWNERS- guarded file into a feature branch, and serialised two same-branch tasks that shared nothing. --- plugins/fd3/CHANGELOG.md | 19 +++++++++ plugins/fd3/skills/split-to-tasks/SKILL.md | 46 ++++++++++++++++++---- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index f756986..14269c6 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -7,8 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `split-to-tasks` accepts a `ready` verdict that carries a declared gap: the blocked claim + becomes an operational task naming its owner, instead of stopping the split; an ownerless + blocked claim still stops it + +### Added + +- `split-to-tasks` cuts a protected path — one guarded by `CODEOWNERS`, branch protection, or a + required review — into a delivery task on its own branch, so one external approval no longer + holds a whole landing unit +- A `depends-on` edge between two tasks on the same branch must name the file or symbol they + share, in the dependent task's `## Note` and in the report; an edge that cannot be named is + dropped, because it only serialises the implementation stage + ### Fixed +- The split reads the spec at its absolute path and never lets the spec's own commit location + decide a branch base — a spec committed on a feature or docs branch no longer roots the stack + there + - CI verdicts now describe the branch they claim to: the toolchain scout reports each command's `cwd` relative to the repository root, the CI prompt `cd`s into the worktree and reads `git branch --show-current`, and `implement-run` / `repair-run` discard a verdict whose branch diff --git a/plugins/fd3/skills/split-to-tasks/SKILL.md b/plugins/fd3/skills/split-to-tasks/SKILL.md index afb11fd..50808d6 100644 --- a/plugins/fd3/skills/split-to-tasks/SKILL.md +++ b/plugins/fd3/skills/split-to-tasks/SKILL.md @@ -26,6 +26,8 @@ that cites nothing — is a reason to stop and report it, never something to fix split. Its frontmatter says `repository: none` and leaves `branch` empty — these fields are machine-read, so prose in them breaks the reader — and its body closes with a `## Note` saying why no pull request exists. + A declared gap the spec carries into the split is the second thing this shape holds (see + *Precondition*): the missing fact has an owner, and the task is how the split tracks it. - **The index card rule** — a task file carries pointers, never copies. The spec stays the single source of truth. Contract prose copied into a task is a second source of truth that rots silently, because nothing detects that the spec moved on. @@ -35,11 +37,17 @@ that cites nothing — is a reason to stop and report it, never something to fix Splitting propagates the spec's defects into every task. A validation verdict in this conversation settles the question. Otherwise the spec must carry all three: read its evidence record **from the bottom** — the last verdict line in the file is the current one, position -decides and not the date — that line records no blocked claims, and its count equals `wc -l` on -the spec. +decides and not the date — that line's blocked claims, if it carries any, are declared gaps, +and its count equals `wc -l` on the spec. -Anything short of that — blocked claims, a count that does not match, a dated block with no -verdict line, no pass anywhere — is a stop before step 1. A dated heading over verified rows is not +**A declared gap is work, not a stop.** A `ready` verdict may carry a blocked claim when +validation recorded it as a declared gap: the fact is unresolved and the spec names who resolves +it. Such a gap gets an **operational task** of its own, whose `## Note` says what has to come +back and from whom, and every task the gap blocks from being *written* draws a `depends-on` edge +onto it (step 4's authorship rule). A blocked claim with no named owner is not a declared gap. + +Anything short of that — an ownerless blocked claim, a count that does not match, a dated block +with no verdict line, no pass anywhere — is a stop before step 1. A dated heading over verified rows is not a verdict. Validating is not this skill's work, and no command is named for it: on *validate first* the split ends with nothing written. What lifts the stop is the user's answer, never your own — say what the record holds, then ask, once, whether to validate first or split as-is. The message that @@ -83,6 +91,11 @@ Record each repository's absolute root path and write that path into `repository not a location: the next stage resolves it by guessing among the user's checkouts, and a feature worked on in a second worktree is exactly where the guess goes wrong. +The spec is an input read at its **absolute path**, and where it happens to be committed decides +nothing. A spec written on a docs branch, on a feature branch, or in a repository the work never +touches still cuts its branches from each repository's default branch: a base is derived from the +rollout and the stack, never from `git log` on the spec file. + ### 2. Cut The raw material is the work items. Every task is a subset of them, and every item lands in @@ -105,6 +118,13 @@ reason, and the reason decides the edge cases: section and becomes its own late task, behind the gate the spec names, carrying `phase: cleanup` in its frontmatter — cleanup is not a rollout phase, and a section reference in a machine-read field breaks the reader. +5. **A protected path cuts, and keeps its own branch.** An edit to a path the repository guards — + a `CODEOWNERS` entry naming another team, a branch-protection or required-review rule, a + release manifest, the CI workflow definitions — waits on an approval the rest of the unit does + not. It is a delivery task like any other, with its own element and done criterion, on a branch + of its own, so one external approval never holds the whole landing unit. Read the repository's + `CODEOWNERS` and protection settings in step 1 to know which paths these are; where the + repository declares none, the rule finds nothing and costs nothing. Within what survives the boundaries, prefer the smallest task that makes one verification row pass. Where one repository owns a whole behaviour, that yields a vertical slice — schema, endpoint @@ -156,7 +176,16 @@ is policy of this skill and never appears in the spec. ### 4. Order Dependencies come from the spec's build order and its phase table. Record each as a `depends-on` -edge between task slugs: an edge means the other task must land first. Never draw an edge onto +edge between task slugs: an edge means the other task must land first. + +**An edge between two tasks on the same branch has to name what they share.** Same-branch tasks +land together, so an edge there is a claim that one must be *written* before the other, and only a +concrete overlap makes that true: a file both touch, a symbol one defines and the other calls, a +migration one writes and the other reads. Name it in the dependent task's `## Note` and in the +report's table — the frontmatter field stays a bare slug list, because prose in a machine-read +field breaks the reader. An edge you cannot name that way is sequencing by intuition: drop it. It +buys nothing on a shared branch and costs the implementation stage a serialisation, since tasks +with no edge between them are implemented concurrently. Never draw an edge onto an operational task when the spec lets the code land before that gate — an edge there strands implementable work behind human hands, and a whole extra run pays for it; a dependency that only gates *verification* belongs in the task's Done-when, not in the graph. A gate that blocks @@ -166,8 +195,9 @@ operational task exists for that gate, that is a coverage failure in step 5, not record the gate in prose. Propose each branch's name following its repository's visible convention — existing branches show it; the name belongs to the group, not the task. One exception joins the step-6 batch: when the checkout already sits on a -branch carrying the spec's commits, whether the first landing unit reuses that branch or cuts -fresh by the convention is the user's call — a user mid-feature may have chosen it deliberately. +branch carrying implementation commits for this spec, whether the first landing unit reuses that +branch or cuts fresh by the convention is the user's call — a user mid-feature may have chosen it +deliberately. A branch that carries only the spec file itself is not that case. When the edge onto an operational task is real, carry it up to the branch: a landing unit that mixes a gate-blocked task with implementable ones cannot reach a complete state in one run. Cut @@ -206,7 +236,7 @@ Before writing anything, check — and say in the report — that: workflow's merge planning relies on that; - the `branch-base` chain is rooted, acyclic, single-parent and identical on every task of a branch. Its one root is the repository's default branch — or, where step 4 found the checkout - already sitting on a branch that carries the spec's commits, that branch: the root is then + already sitting on a branch that carries implementation commits for this spec, that branch: the root is then whatever the step-6 answer settles, so a chain rooted there is a question still pending, never a coverage failure. Stopping on it would abort a split the user was never asked about. From 2fd3bef4bcaab288b3d03c23a88c3576f5d30def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:23:23 +0200 Subject: [PATCH 11/44] fix(fd3): make a ready verdict mean ready, and re-validate what a pass rewrote A pass returned ready while carrying blocked claims the split then refused, and a pass that edited the spec closed the loop on a document nothing re-read. --- plugins/fd3/CHANGELOG.md | 9 +++++++++ plugins/fd3/commands/build-spec.md | 2 +- plugins/fd3/references/validation-report.md | 19 ++++++++++++++++++- plugins/fd3/skills/validate-spec/SKILL.md | 11 +++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 14269c6..59c9f8e 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -24,6 +24,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A `ready` verdict no longer hides an ownerless gap: `validate-spec` returns `ready` only when + every claim is `verified` or `deferred`, and asking the user for an owner is the last move + before a claim is recorded `blocked` +- `build-spec` re-invokes validation when the previous pass edited the spec, not only when the + verdict was not ready — a `ready` verdict on a document that same pass rewrote judged the + version before those edits — and each re-invocation carries a focus list of open findings and + edited sections +- The validation return prints all twelve check rows with their fixed numbering and short names, + and the report reference says to re-read it after a compaction - The split reads the spec at its absolute path and never lets the spec's own commit location decide a branch base — a spec committed on a feature or docs branch no longer roots the stack there diff --git a/plugins/fd3/commands/build-spec.md b/plugins/fd3/commands/build-spec.md index 613e536..a3a6912 100644 --- a/plugins/fd3/commands/build-spec.md +++ b/plugins/fd3/commands/build-spec.md @@ -7,6 +7,6 @@ Three stages, in order. The grilling runs here, in the main thread — it needs 1. Invoke the `fd3:grill-topic` skill with `$ARGUMENTS`, forwarded verbatim. Work its rounds to the end. While its lookups run, speak only when there is something to decide — a round ready to post, a returning fact that voids a question already asked, or a command the user must run; a lookup that came back and changed nothing gets one line. 2. Once the user confirms the closing summary — and not before — invoke the `fd3:write-spec` skill with the path of the closing-notes file the grilling wrote, the session's research directory, and where the spec goes. The paths, never a description of where things are. It runs in its own context; do not wrap it in a sub-agent of your own. Relay its report: where the spec went, the counts, what is not yet settled in it. If it stops to ask instead, put its questions to the user here, batched per `${CLAUDE_PLUGIN_ROOT}/references/question-batching.md`, and send the answers back with `SendMessage` — it resumes where it stopped. -3. Then invoke the `fd3:validate-spec` skill on the written spec. It runs in its own context and returns its verdict and status. Validation is also where the spec's defects get repaired: the skill edits the spec wherever a fact settles one, so its report lists spec edits alongside findings. A verdict on a spec that had findings and lists no edits is a pass that only counted them — say so when you relay it. While the verdict is not ready and a further pass could close what remains, invoke it again on the same spec, passing back the status it returned — at most three passes in all, and say plainly what is still open if the third ends short. A pass that stopped to ask is not finished: resume it with `SendMessage`, never with a fresh invocation. A pass that ended its own turn with a verdict is finished, and the next invocation is a fresh reading of a document the previous pass edited — the only thing that ever re-checks those edits, so do not skip it because the verdict looks close. If it stops to ask instead, put its questions to the user here, batched per `${CLAUDE_PLUGIN_ROOT}/references/question-batching.md`, and send the answers back with `SendMessage` — it resumes where it stopped. Relay the final verdict, and relay every finding the status leaves standing inside a passing check. +3. Then invoke the `fd3:validate-spec` skill on the written spec. It runs in its own context and returns its verdict and status. Validation is also where the spec's defects get repaired: the skill edits the spec wherever a fact settles one, so its report lists spec edits alongside findings. A verdict on a spec that had findings and lists no edits is a pass that only counted them — say so when you relay it. Invoke it again on the same spec, passing back the status it returned, while either holds: the verdict is not ready and a further pass could close what remains, or the pass applied spec edits — a `ready` verdict on a document the same pass rewrote is a verdict on the version before those edits, and nothing but the next pass ever reads them. At most three passes in all, and say plainly what is still open if the third ends short. Each re-invocation carries a **focus list** alongside the status: the findings the previous pass left open and the sections it edited. That is what the next pass spends itself on; everything the status records as verified it inherits. A pass that stopped to ask is not finished: resume it with `SendMessage`, never with a fresh invocation. A pass that ended its own turn with a verdict is finished, and the next invocation is a fresh reading of a document the previous pass edited — the only thing that ever re-checks those edits, so do not skip it because the verdict looks close. If it stops to ask instead, put its questions to the user here, batched per `${CLAUDE_PLUGIN_ROOT}/references/question-batching.md`, and send the answers back with `SendMessage` — it resumes where it stopped. Relay the final verdict, and relay every finding the status leaves standing inside a passing check. Confirmation of shared understanding is the gate between stages 1 and 2. If the user ends the session without confirming, stop after the first stage and say what is still open. diff --git a/plugins/fd3/references/validation-report.md b/plugins/fd3/references/validation-report.md index cb66632..4b8e0f9 100644 --- a/plugins/fd3/references/validation-report.md +++ b/plugins/fd3/references/validation-report.md @@ -1,6 +1,8 @@ # Validation report -The shape `validate-spec` returns its verdict and its status in. +The shape `validate-spec` returns its verdict and its status in. Read this file before composing a +return, and read it again after a compaction — a return composed from memory is where the fixed +rows and the four `Result` forms go missing. ```markdown ## Run @@ -16,10 +18,25 @@ repositories: |---|---|---| ## Checks + +Twelve rows, always all twelve, numbered and ordered as the skill numbers them — a return that +prints only the checks that moved leaves the caller unable to tell an unrun check from a passing +one. The short names are fixed too: + | # | Check | Result | |---|---|---| | 1 | decisions do not contradict | pass (unchanged) | | 2 | scope covers every decision | fail — section 10, | +| 3 | every element described and coded | | +| 4 | dependencies exist, planned or deferred | | +| 5 | external contracts confirmed | | +| 6 | referenced documents open | | +| 7 | element contracts complete | | +| 8 | build order stated and holds | | +| 9 | achievable in this project | | +| 10 | splittable into tasks | | +| 11 | every element has a check | | +| 12 | no vague verb, no undecided either/or | | ## Still open - —

— — blocking | non-blocking diff --git a/plugins/fd3/skills/validate-spec/SKILL.md b/plugins/fd3/skills/validate-spec/SKILL.md index 1b7bc5a..8d992a2 100644 --- a/plugins/fd3/skills/validate-spec/SKILL.md +++ b/plugins/fd3/skills/validate-spec/SKILL.md @@ -62,6 +62,10 @@ broken one — so a check inherits its reasoning, never its result. What you do evidence work behind a claim the status records as `verified`, unless an edit since then touched the section it rests on. Spend the pass on what the status leaves open. +The invocation may carry a **focus list** with that status — the findings still open and the sections +the previous pass edited. It says where to spend the pass, never what to skip: the twelve checks are +re-derived either way, and an edited section is read as new text, not as a section already cleared. + The spec's evidence record may hold dated blocks that no handed-down status accounts for — passes from earlier runs. Their identity is their date; pass numbers count this run's passes only. They are available when a claim's history bears on what you are deciding. @@ -274,6 +278,13 @@ names no owner. A `blocked` claim goes into the report; do not put it to the use ends with a claim `open`: a claim you cannot settle before reporting becomes `blocked`, with the reason it could not be settled stated in the report. +**`ready` and `blocked`.** The verdict is `ready` only when every claim is `verified` or `deferred` — +a declared gap with a named owner and a placement is what a downstream stage can act on, because +`split-to-tasks` turns it into an operational task carrying that owner. An ownerless `blocked` claim +leaves it nothing to write, so it makes the verdict `not ready`, however small the gap looks. The +last move before recording a claim `blocked` is therefore step 4: ask the user who owns it and where +it lands. An owner and a placement make it `deferred`, and the spec records both. + ### 6. Report End the pass by returning the verdict and this pass's status — nothing is written to a file. The From c44142285716537059559c0fedc139864feb33f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:24:16 +0200 Subject: [PATCH 12/44] feat(fd3): keep the session's facts and questions in files, and cap table-cell prose A run lost its pre-command constraints and its carried-over question numbers to a compaction, and wrote spec tables whose cells had grown into paragraphs. --- plugins/fd3/CHANGELOG.md | 6 ++++++ plugins/fd3/references/spec-template.md | 10 ++++++++++ plugins/fd3/skills/grill-topic/SKILL.md | 4 ++++ plugins/fd3/skills/write-spec/SKILL.md | 4 ++++ 4 files changed, 24 insertions(+) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 59c9f8e..7ae7ad1 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `grill-topic` writes what the conversation established before the command into the research + directory before round 1, and keeps a question ledger file — round numbers, answers and + carry-overs no longer live only in a context that gets compacted +- The spec template caps every table cell at two sentences and sends longer evidence to + `evidence/
.md`, with a per-pass overflow file for a validation block that outgrows the + appendix - `split-to-tasks` cuts a protected path — one guarded by `CODEOWNERS`, branch protection, or a required review — into a delivery task on its own branch, so one external approval no longer holds a whole landing unit diff --git a/plugins/fd3/references/spec-template.md b/plugins/fd3/references/spec-template.md index 540f597..d9879f1 100644 --- a/plugins/fd3/references/spec-template.md +++ b/plugins/fd3/references/spec-template.md @@ -227,3 +227,13 @@ A table. This is the spec's proof of work, and it is what a validation pass spot A claim that rests on inference says so — "no documentation states the negative explicitly; treat as strong inference, confirmed empirically at stage before prod" is honest and actionable. Softening it into a confirmation is the one thing this table exists to prevent. + +**A cell is a line, not a paragraph.** Two sentences at most in any cell of any table in this +document. A verification that needs a transcript, a query plan, a long quote or a list of hits puts +it in `evidence/
.md` beside the spec and cites that file in the cell. Prose that fills a +cell is unreadable at the width a reviewer scans, and it is what pushes a spec past the size at +which anyone re-reads it. + +Each validation pass appends its own dated block here. A pass whose rows outgrow the appendix writes +them to `evidence/-pass-.md` and leaves the dated block its verdict line plus one line per +claim pointing at that file — the record stays complete, and the spec stays a document. diff --git a/plugins/fd3/skills/grill-topic/SKILL.md b/plugins/fd3/skills/grill-topic/SKILL.md index 221455b..c627b6b 100644 --- a/plugins/fd3/skills/grill-topic/SKILL.md +++ b/plugins/fd3/skills/grill-topic/SKILL.md @@ -16,6 +16,8 @@ Establish the facts the topic asserts, before asking anything. A topic document This is the highest-value work in the whole session. A round asked against the document gets answers about the document; a round asked against reality gets answers you can build on. +**The conversation before the command counts as input.** Whatever was established before this skill was invoked — a constraint the user stated, an option they already ruled out, a number they gave, a file they pointed at — goes into a file in the session's research directory before round 1, one line each, naming who established it. It is the only input no lookup can re-derive: it does not survive a compaction, and every downstream skill reads files rather than this conversation. Round 1 then treats those lines as facts under test like any other, not as settled ground. + Two things come before the first dispatch. `git fetch` the repository and say if the tree is behind the branch the topic describes — facts cited from a stale clone drift on exactly the files the session will argue from. And read the repository's own prior specs, ADRs and decision records (`docs/specs/`, `requirements/`, wherever they live): a question one of them already settles is not a question, and a lookup dispatched without them re-researches a decision the repository has already made. For every library, service or tool the topic names, establish three versions: the one the lockfile resolves — never the manifest range — the current release, and the one whose API the discussion will quote. Any difference between them is a round-1 finding: an API argued from the wrong version becomes pseudocode nobody can run. A table when there are more than two, a line each otherwise. @@ -40,6 +42,8 @@ Open every round after the first with one line naming the numbers still unanswer A blocked question keeps its number and stays out of the round's numbered items; name it on the line that opens the round. A number printed inside the round is a number the user will answer. +Keep a **question ledger** file in the session scratchpad, one row per number: the question in a line, the round it went out in, the option chosen or `open`, and `carried-over` where it has been re-asked. Write the row when the question goes out, and update it when the answer arrives — before composing the next round, which is read from the ledger and not from memory. Numbers tracked in your head are the first thing a compaction takes, and what comes back after one is a reassigned number, a question that quietly vanished, or a carried-over question compressed into a summary of itself. + Track which numbered questions came back answered. A question the user skipped is still open: re-put it in the next round under its original number, labelled as carried over, with its options and costs written out in full — a carried-over question compressed to a list of recommendations is not a question, and a user who answers one is ratifying a menu they cannot see. Numbers are never reassigned, so the summary can cite one decision by one name. Silence is not assent, and there is no round count after which it becomes assent. A defect the user admits to scope is work admitted, not work decided: each admitted defect gets its own numbered question with fix options, or an explicit deferral with an owner. A batch yes/no that sweeps a dozen defects into scope leaves every one of them undesigned. diff --git a/plugins/fd3/skills/write-spec/SKILL.md b/plugins/fd3/skills/write-spec/SKILL.md index 00f2f3a..8f7584c 100644 --- a/plugins/fd3/skills/write-spec/SKILL.md +++ b/plugins/fd3/skills/write-spec/SKILL.md @@ -101,6 +101,10 @@ worth more than any prose you could write instead. Two rules: `${CLAUDE_SKILL_DIR}/../../references/fact-routes.md`. If it stays unsettled, it goes into the document as a declared gap with an owner and a placement, never as a bare statement. +Keep every cell of this table — and of every other table in the spec — to two sentences. Evidence +that needs more room goes to `evidence/
.md` beside the spec, cited from the cell; the +template says so, and a spec whose tables read as prose is one nobody re-reads. + A number you chose while writing — a bake period, a waiting window, a threshold, a version — is a claim like any other: it gets an evidence row stating its basis, or it becomes a declared gap. A plausible reason attached to a number nobody agreed is still a number nobody agreed. From 3e2e3f2d2cb48e590ad0f5c4f589418cbc4d2790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:24:49 +0200 Subject: [PATCH 13/44] fix(fd3): diagnose by running the check, and name the worktrees at the close A run composed a repair from a grep-shaped guess, asked its repair agent to validate alongside the workflow, and ended without saying where any branch's worktree lived. --- plugins/fd3/CHANGELOG.md | 6 ++++++ plugins/fd3/skills/implement-tasks/SKILL.md | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 7ae7ad1..4d20412 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -39,6 +39,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 edited sections - The validation return prints all twelve check rows with their fixed numbering and short names, and the report reference says to re-read it after a compaction +- A HIL CI failure is diagnosed by running the failing check in the branch's worktree, not by + grepping the source for what the message suggests +- A repair `instructions` line says what to change and never asks the agent to validate — the + workflow runs CI itself, and a second pipeline on the machine is exactly what validation cannot + tolerate +- The closing proposal names each branch's worktree path, whatever the user decides about pushing - The split reads the spec at its absolute path and never lets the spec's own commit location decide a branch base — a spec committed on a feature or docs branch no longer roots the stack there diff --git a/plugins/fd3/skills/implement-tasks/SKILL.md b/plugins/fd3/skills/implement-tasks/SKILL.md index 9c1fd2e..ad8ced0 100644 --- a/plugins/fd3/skills/implement-tasks/SKILL.md +++ b/plugins/fd3/skills/implement-tasks/SKILL.md @@ -212,7 +212,10 @@ the task file's steps as a script to follow; mark `done` only when they confirm) conflict needs their call on how to proceed. A CI failure on the list may be diagnosed first — read-only, in the branch's worktree — so the question puts analyzed options before the user instead of raw output; the diagnosis then travels verbatim in the repair `instructions`, sparing -the repair agent a re-investigation. The answers split into two lanes: +the repair agent a re-investigation. Diagnose by **running the failing check** in that worktree and +reading what it says. Grepping the source for what the report's message suggests names a plausible +cause, not the cause: the check is the only thing that knows which of them is true, and a repair +composed from the plausible one costs a full round to disprove. The answers split into two lanes: - **Decisions that unblock tasks** — update the affected task files and relaunch `implement-run` the same way; statuses make the rerun skip everything finished. @@ -238,6 +241,10 @@ the repair agent a re-investigation. The answers split into two lanes: the path, never the knowledge. Repair agents receive the decision as their sole authority and never read the spec. Repair validation is CI only — no code review. + An `instructions` line says what to change, never asks for validation. "Then run the tests and + confirm they pass", "verify the build is green" — the workflow runs CI itself, after the agent + returns, and an agent that runs it too puts a second pipeline on a machine that tolerates one. + One carve-out from the second lane: a purely mechanical git operation — merging an existing task branch into its target, reverting a named commit — may be done by this skill directly when the decision deliberately leaves the branch incomplete, because a repair-run would fail its own @@ -257,8 +264,8 @@ live. A pause that survives only in this conversation is state lost. ### 5. Propose, never push When every repository-bearing task is `done`: one table — repository, branch, its stack base, -tasks on it, the element codes those tasks carry, proposed pull-request title citing the -tickets — with the still-open operational tasks listed alongside; they need the branches landed +its worktree path, tasks on it, the element codes those tasks carry, proposed pull-request title +citing the tickets — with the still-open operational tasks listed alongside; they need the branches landed first, so they never gate this proposal. Stacked branches make a pull-request chain: each pull request's base is its branch's stack base, and after one lands its successor is retargeted onto the default branch — but only when the predecessor landed as a merge commit. After a squash @@ -269,6 +276,8 @@ after explicit consent: push, `gh pr create` per branch (`--base` set to the sta description naming the tasks, the spec and the branch's element codes. Offer cleanup — remove the `.worktrees` directories and delete the merged `task/` branches — as its own question, never coupled to the push: declining to publish while wanting a clean repository is a -normal combination. If push consent does not come, leave everything local and say where it -lives — and when the tasks directory is untracked, say that too: it is the only copy of the +normal combination. If push consent does not come, leave everything local. The worktree paths are +in the table whatever the user decides: a branch whose worktree nobody can name is a branch the +user cannot open, and the run's own directories are not guessable. When the tasks directory is +untracked, say that too: it is the only copy of the run's state store, one `git clean -fd` away from gone. From 987b362ef84cb36851ed5ec5239191fba30fa61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Tue, 22 Sep 2026 23:25:35 +0200 Subject: [PATCH 14/44] docs(code-review): describe the spec lens gate as a named spec, not only the flag --- plugins/code-review/CONTEXT.md | 6 +++--- plugins/code-review/README.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/code-review/CONTEXT.md b/plugins/code-review/CONTEXT.md index 7c175c8..df1fd2c 100644 --- a/plugins/code-review/CONTEXT.md +++ b/plugins/code-review/CONTEXT.md @@ -15,7 +15,7 @@ One parallel review subagent running exactly one Lens. _Avoid_: role, reviewer, worker **Lens**: -One of the eight rule clusters: comments (`R1`–`R12`), readability & tests, naming & module, objects & patterns, simplicity & types, security, performance, spec. Three sit beyond the craft five: security (always on), and two gated ones — performance (executable source files in scope), spec (`--spec ` given). Comments is a Lens like any other, not a special case; the three added Lenses have no standalone skill. +One of the eight rule clusters: comments (`R1`–`R12`), readability & tests, naming & module, objects & patterns, simplicity & types, security, performance, spec. Three sit beyond the craft five: security (always on), and two gated ones — performance (executable source files in scope), spec (a spec file named — `--spec `, or the one the diff carries, accepted by the user). Comments is a Lens like any other, not a special case; the three added Lenses have no standalone skill. _Avoid_: theme, dimension **Rules file**: @@ -42,7 +42,7 @@ The root pair `CODING_STANDARDS.md` + `CODING_STANDARDS.local.md`, read together _Avoid_: style guide, conventions file (a conventions file — `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`, `.claude/rules`, `.cursor/rules` — only suppresses) **Active lens set**: -The N Lenses (6 to 8) that Step 2b of **start-cr** resolves for one run: the five craft Lenses and security always, performance when the source-kind subset of the resolved files (minus `.sh`) is non-empty, spec when `--spec` was given. The report's `Lenses: L of 8` line records it, naming every inactive Lens with its reason. +The N Lenses (6 to 8) that Step 2b of **start-cr** resolves for one run: the five craft Lenses and security always, performance when the source-kind subset of the resolved files (minus `.sh`) is non-empty, spec when a spec file was named. The report's `Lenses: L of 8` line records it, naming every inactive Lens with its reason. _Avoid_: lens selection (the user never picks), enabled lenses ## Relationships @@ -60,7 +60,7 @@ _Avoid_: lens selection (the user never picks), enabled lenses ## Example dialogue > **Dev:** "Can I run just the comment **Scanner**?" -> **Domain expert:** "Invoke the `comment-review` skill directly — **start-cr** always runs its whole **Active lens set**; a **Scanner** is its internal unit of fan-out, not a user-facing switch. The set is decided by the change and the `--spec` flag, never by picking lenses." +> **Domain expert:** "Invoke the `comment-review` skill directly — **start-cr** always runs its whole **Active lens set**; a **Scanner** is its internal unit of fan-out, not a user-facing switch. The set is decided by the change and by whether a spec file is named, never by picking lenses." ## Flagged ambiguities diff --git a/plugins/code-review/README.md b/plugins/code-review/README.md index 49258b6..7e7142d 100644 --- a/plugins/code-review/README.md +++ b/plugins/code-review/README.md @@ -28,7 +28,8 @@ From the `grixu/cc-toolkit` marketplace: `/start-cr` has no lens switch; the change decides which lenses run. The five craft lenses and `security` run every time; `performance` runs when the change -touches executable source; `spec` runs only when you pass `--spec `. The +touches executable source; `spec` runs when a spec file is named — by `--spec `, +or by accepting the one the review offers when the diff itself carries a spec. The report's `Lenses: L of 8` line names every lens that sat out and why. For a partial review, invoke `/comment-review` or `/quality-review` directly; both stay independently available and share the same rule text as the command. The three @@ -86,7 +87,8 @@ says which ran: async path, wasted React renders. Every finding names the multiplier, the call inside it, the missing bound, and the batch/limit API that exists; "could be slow" is not a finding. -- **spec** — only with `--spec ` (a local file). A spec line nothing +- **spec** — only with a named spec file (`--spec `, or the one the review + offers from the diff). A spec line nothing implements, one implemented against its wording, one only partly met, and scope creep the spec never asked for. Every finding quotes the spec line. From 811d6341eb6fec37ac604eecd120fc721056435a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 00:10:05 +0200 Subject: [PATCH 15/44] fix(fd3): pin the grill session's two bookkeeping files to notes/ and research/ The eval gate reads any other .md the grilling writes as a spec written before confirmation, and an unplaced ledger is also a file the next skill cannot find. --- plugins/fd3/skills/grill-topic/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/fd3/skills/grill-topic/SKILL.md b/plugins/fd3/skills/grill-topic/SKILL.md index c627b6b..4c41cb2 100644 --- a/plugins/fd3/skills/grill-topic/SKILL.md +++ b/plugins/fd3/skills/grill-topic/SKILL.md @@ -16,7 +16,7 @@ Establish the facts the topic asserts, before asking anything. A topic document This is the highest-value work in the whole session. A round asked against the document gets answers about the document; a round asked against reality gets answers you can build on. -**The conversation before the command counts as input.** Whatever was established before this skill was invoked — a constraint the user stated, an option they already ruled out, a number they gave, a file they pointed at — goes into a file in the session's research directory before round 1, one line each, naming who established it. It is the only input no lookup can re-derive: it does not survive a compaction, and every downstream skill reads files rather than this conversation. Round 1 then treats those lines as facts under test like any other, not as settled ground. +**The conversation before the command counts as input.** Whatever was established before this skill was invoked — a constraint the user stated, an option they already ruled out, a number they gave, a file they pointed at — goes into `prior-conversation.md` in the session's research directory before round 1, one line each, naming who established it. It is the only input no lookup can re-derive: it does not survive a compaction, and every downstream skill reads files rather than this conversation. Round 1 then treats those lines as facts under test like any other, not as settled ground. Two things come before the first dispatch. `git fetch` the repository and say if the tree is behind the branch the topic describes — facts cited from a stale clone drift on exactly the files the session will argue from. And read the repository's own prior specs, ADRs and decision records (`docs/specs/`, `requirements/`, wherever they live): a question one of them already settles is not a question, and a lookup dispatched without them re-researches a decision the repository has already made. @@ -42,7 +42,7 @@ Open every round after the first with one line naming the numbers still unanswer A blocked question keeps its number and stays out of the round's numbered items; name it on the line that opens the round. A number printed inside the round is a number the user will answer. -Keep a **question ledger** file in the session scratchpad, one row per number: the question in a line, the round it went out in, the option chosen or `open`, and `carried-over` where it has been re-asked. Write the row when the question goes out, and update it when the answer arrives — before composing the next round, which is read from the ledger and not from memory. Numbers tracked in your head are the first thing a compaction takes, and what comes back after one is a reassigned number, a question that quietly vanished, or a carried-over question compressed into a summary of itself. +Keep a **question ledger** — `notes/question-ledger.md` in the session scratchpad, beside where the closing notes land — with one row per number: the question in a line, the round it went out in, the option chosen or `open`, and `carried-over` where it has been re-asked. Write the row when the question goes out, and update it when the answer arrives — before composing the next round, which is read from the ledger and not from memory. Numbers tracked in your head are the first thing a compaction takes, and what comes back after one is a reassigned number, a question that quietly vanished, or a carried-over question compressed into a summary of itself. Track which numbered questions came back answered. A question the user skipped is still open: re-put it in the next round under its original number, labelled as carried over, with its options and costs written out in full — a carried-over question compressed to a list of recommendations is not a question, and a user who answers one is ratifying a menu they cannot see. Numbers are never reassigned, so the summary can cite one decision by one name. Silence is not assent, and there is no round count after which it becomes assent. From 0c0d4302ee46afb94070bb747517dad96688f40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 00:40:28 +0200 Subject: [PATCH 16/44] fix(code-review): keep the security CANDIDATES block out of a code fence A fenced block reads as source to the merge step; the eval caught the scanner emitting one. --- plugins/code-review/CHANGELOG.md | 1 + plugins/code-review/references/rules/security.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 1da4a57..0a47c15 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 change and another (`origin/main`, …) holds commits — a branch pushed to its own remote counterpart no longer reads as "nothing to review"; `start-cr`, `comment-review` and `quality-review` offer the re-run instead of stopping +- A `security` scanner's `CANDIDATES` block is a heading with bullets, never a fenced code block - Scope: `.mjs`/`.cjs`/`.mts`/`.cts` are reviewed as source, a directory whose name contains `e2e` (or ends in `-tests`) classifies as `test`, `.txt` is skipped, and a CI workflow file is skipped with a sentence naming its triggers, permissions and secret diff --git a/plugins/code-review/references/rules/security.md b/plugins/code-review/references/rules/security.md index 2e0e3ad..a336c3c 100644 --- a/plugins/code-review/references/rules/security.md +++ b/plugins/code-review/references/rules/security.md @@ -52,7 +52,8 @@ Every rule below carries its **Flag** conditions, a **Suggested fix**, and a builder parameterizing, does this decorator check ownership? Name the pair; a candidate with no named pair is a cleared prose line. Cleared look-alikes go to `Not flagged`, one prose line naming the pair and the mitigation — never the finding - shape. + shape. The block is a heading and markdown bullets, never a fenced code block: a fence + makes the orchestrator's merge read it as source rather than as findings. - **Never run the code; never run `npm audit`, a secret scanner, or any network command.** The evidence is the lines in view and what Read/Grep return. - **`.env`, yaml, JSON, manifests, and lockfiles are out of this lens** — skipped by From 93e09e66578a47e3686e3a11356b0a045621c75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 00:46:08 +0200 Subject: [PATCH 17/44] fix(fd3): keep declared gaps out of the question batch and out of the verdict The eval suite caught both regressions from this batch: validate handed up gaps the spec already owned, and split grew an operational task per phase boundary where the spec names no gate. --- plugins/fd3/CHANGELOG.md | 4 ++++ plugins/fd3/skills/split-to-tasks/SKILL.md | 3 +++ plugins/fd3/skills/validate-spec/SKILL.md | 8 +++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 4d20412..2685f55 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A gap the spec already declares with an owner and a placement is `deferred` on sight — it never + reaches the user as a question, and it never lowers a verdict or a phase row +- An operational task exists only for hand-run steps no repository carries; a phase's own + verification rows are run by the repositories' checks and raise no task - A `ready` verdict no longer hides an ownerless gap: `validate-spec` returns `ready` only when every claim is `verified` or `deferred`, and asking the user for an owner is the last move before a claim is recorded `blocked` diff --git a/plugins/fd3/skills/split-to-tasks/SKILL.md b/plugins/fd3/skills/split-to-tasks/SKILL.md index 50808d6..952f86b 100644 --- a/plugins/fd3/skills/split-to-tasks/SKILL.md +++ b/plugins/fd3/skills/split-to-tasks/SKILL.md @@ -28,6 +28,9 @@ that cites nothing — is a reason to stop and report it, never something to fix them breaks the reader — and its body closes with a `## Note` saying why no pull request exists. A declared gap the spec carries into the split is the second thing this shape holds (see *Precondition*): the missing fact has an owner, and the task is how the split tracks it. + Both cases need hand-run steps that no repository carries. A phase's own verification rows are + not that — the repositories' checks run them — so a phase boundary gets no operational task + unless the spec names a gate outside its own verification. - **The index card rule** — a task file carries pointers, never copies. The spec stays the single source of truth. Contract prose copied into a task is a second source of truth that rots silently, because nothing detects that the spec moved on. diff --git a/plugins/fd3/skills/validate-spec/SKILL.md b/plugins/fd3/skills/validate-spec/SKILL.md index 8d992a2..5825e3a 100644 --- a/plugins/fd3/skills/validate-spec/SKILL.md +++ b/plugins/fd3/skills/validate-spec/SKILL.md @@ -278,13 +278,19 @@ names no owner. A `blocked` claim goes into the report; do not put it to the use ends with a claim `open`: a claim you cannot settle before reporting becomes `blocked`, with the reason it could not be settled stated in the report. -**`ready` and `blocked`.** The verdict is `ready` only when every claim is `verified` or `deferred` — +**`ready` and `blocked`.** A `deferred` claim never lowers a verdict: it bounds the phase it gates, +that phase's row still reads `yes`, and the document is still `ready`. The verdict is `ready` only +when every claim is `verified` or `deferred` — a declared gap with a named owner and a placement is what a downstream stage can act on, because `split-to-tasks` turns it into an operational task carrying that owner. An ownerless `blocked` claim leaves it nothing to write, so it makes the verdict `not ready`, however small the gap looks. The last move before recording a claim `blocked` is therefore step 4: ask the user who owns it and where it lands. An owner and a placement make it `deferred`, and the spec records both. +This reaches the user only for a claim **nothing in the spec owns**. A gap the spec already declares +with an owner and a placement is `deferred` on sight — it is not a finding, it does not go into the +batch, and a pass that asks about it has turned a settled document into a question. + ### 6. Report End the pass by returning the verdict and this pass's status — nothing is written to a file. The From 38fdec52ab6a61c45ac9fa3003d2af46ad6544df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 05:31:53 +0200 Subject: [PATCH 18/44] fix(code-review): resolve a spec-id-shaped token against the code before stripping it --- plugins/code-review/CHANGELOG.md | 3 +++ plugins/code-review/references/rules/comments.md | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 0a47c15..230f016 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -59,6 +59,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - When no `--spec` was passed and the diff carries a spec-shaped file (`specs/`, `docs/adr/`, `tasks/`, `*SPEC*.md`, …), `start-cr` offers to review the change against it instead of silently leaving the `spec` lens off +- `comments` · R4 resolves a spec-id-shaped token against the code before stripping it — a + token bound to an identifier or a string literal is a code value, not a document pointer, + and the verdict line says which of the two it was ### Added diff --git a/plugins/code-review/references/rules/comments.md b/plugins/code-review/references/rules/comments.md index 7d15dfb..53708e8 100644 --- a/plugins/code-review/references/rules/comments.md +++ b/plugins/code-review/references/rules/comments.md @@ -125,6 +125,15 @@ hides coupling: the reader has to leave the code to understand the code. ``` The fragment is the leak: it is fine to keep the real constraint, but the `(R2)`, the `F1:`, the `§4.1` must not ride along into the kept comment. +- **Resolve the token against the code before you strip it, and say how it + resolved.** A letter+number reads like a spec-id and can just as easily be a + value the code uses — a region (`R2`), a tier, an enum member, a column name. + `Grep` the token in the file and its neighbours: bound to an identifier or a + string literal, it is a code value and the comment naming it is not an R4 + finding at all; found nowhere in the code, it points into a document and the + strip applies. The verdict line says which of the two it was, in a clause — + a strip whose reasoning is "it looks like a spec-id" is the one way this rule + deletes a fact the reader needed. - **No "provenance" loophole.** A doc/file ref glued onto an otherwise self-contained sentence still goes (`// that hard-stop is intentional (DD_PLAN.md T4.1)` → `// that hard-stop is intentional`). The test is simple: From 4de88b89cd34a0eb918b7cea4f55175eae0fac66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 05:32:07 +0200 Subject: [PATCH 19/44] fix(code-review): require the Not flagged line whenever a look-alike was cleared --- plugins/code-review/CHANGELOG.md | 3 +++ plugins/code-review/skills/quality-review/SKILL.md | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 230f016..26d2d2b 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -62,6 +62,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `comments` · R4 resolves a spec-id-shaped token against the code before stripping it — a token bound to an identifier or a string literal is a code value, not a document pointer, and the verdict line says which of the two it was +- `Not flagged` is required whenever a look-alike was cleared, and a clean file is the case + that needs it most: without the line a reader cannot tell a review that cleared candidates + from one that never looked ### Added diff --git a/plugins/code-review/skills/quality-review/SKILL.md b/plugins/code-review/skills/quality-review/SKILL.md index 388a351..82c35c2 100644 --- a/plugins/code-review/skills/quality-review/SKILL.md +++ b/plugins/code-review/skills/quality-review/SKILL.md @@ -259,7 +259,7 @@ kind of noise. So render the report with **exactly this template**, in this orde ### - `family` · rule · severity · L — <…> -**Not flagged:** +**Not flagged:** **Boy-scout (untouched code, optional):** - `family` · rule · :L — @@ -324,6 +324,11 @@ Rules for filling it in: the wall-of-text this format exists to kill. The full refactor belongs in Step 4 (apply time) or when the user asks to see it. If a fix genuinely cannot be named without a few tokens of code, inline at most a short expression. +- **`Not flagged` is not optional when you cleared something.** A clean file is the case + that most needs it: with no findings to read, the line is the only evidence that the + look-alikes were considered rather than missed, and a reader cannot tell a review that + cleared six candidates from one that never looked. Name them in the line, never only in + the prose of your own reasoning. - **`Not flagged`** is **one line** — a comma-separated list of the look-alikes you considered and passed on, not a paragraph per item. The exception is an entry that is a *real* problem with no rule to land on: that one keeps its own bullet, since From 60575efe9d7c29d0b955e03e420910f4e4696d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 05:32:14 +0200 Subject: [PATCH 20/44] fix(fd3): send the pass report with the question batch, and edit only what a finding names --- plugins/fd3/CHANGELOG.md | 5 +++++ plugins/fd3/skills/validate-spec/SKILL.md | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 2685f55..5510371 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -43,6 +43,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 edited sections - The validation return prints all twelve check rows with their fixed numbering and short names, and the report reference says to re-read it after a compaction +- A pass that hands questions up sends its report with them — the twelve rows, the findings it + already holds and a `not ready` verdict, with the handed-up items under *Still open* — instead + of promising the table once answers land that may never come +- `validate-spec` edits only what a finding of that pass names: on a spec whose checks all pass it + leaves the file byte-identical apart from the appended evidence block - A HIL CI failure is diagnosed by running the failing check in the branch's worktree, not by grepping the source for what the message suggests - A repair `instructions` line says what to change and never asks the agent to validate — the diff --git a/plugins/fd3/skills/validate-spec/SKILL.md b/plugins/fd3/skills/validate-spec/SKILL.md index 5825e3a..4c1b23c 100644 --- a/plugins/fd3/skills/validate-spec/SKILL.md +++ b/plugins/fd3/skills/validate-spec/SKILL.md @@ -14,6 +14,12 @@ need, then end your turn. Do not guess it and do not go looking for it. **That spec file is the only file you may edit.** Everything else you read is read-only, no matter what you find in it. +**Every edit traces to a finding of this pass.** The dated evidence block is appended to a spec of +any quality; everything else you write must be the repair of something you recorded as a finding, +in the section that finding names. A spec whose checks all pass leaves this skill byte-identical +except for the appended block — rewording a section you merely read, tidying a table, or improving +prose nobody flagged rewrites a document the user validated on the strength of its own wording. + ## Goal Decide whether the spec can be implemented, or split into tasks, as written. It can when every @@ -261,6 +267,12 @@ repair choices alike — numbered, each with your recommended answer first, then answers arrive as a message and you continue from where you stopped, with everything this pass established still in front of you. +**The batch carries this pass's report with it, and so does the turn that ends.** Handing up is not +an alternative to reporting: the twelve check rows, the findings you already hold and a verdict of +`not ready` go out in the same message, with the handed-up items under *Still open*. A turn that +ends on "the full table comes once the answers land" leaves the caller with nothing to relay and +nothing to act on, and the answers may never come. + One batch per pass. Nothing may still be outstanding when you send it: a dispatch that has not returned is a dispatch whose answer changes what you would ask, and a second message sent while the first is being answered tells the user the first was incomplete. Steps 3–5 may bring you back here, From ab36f9a2cc3ef05a40ea85dbbdfa93d1c3164aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 05:39:45 +0200 Subject: [PATCH 21/44] test(code-review): add iac, access-widening, guard-routing and scope scenarios --- plugins/code-review/CHANGELOG.md | 4 + .../evals/fixtures/access-widening.diff | 59 +++++++++ .../evals/fixtures/iac-exposure.tf | 104 ++++++++++++++++ .../scope-mix/.github/workflows/ci.yml | 21 ++++ .../evals/fixtures/scope-mix/build.mjs | 28 +++++ .../fixtures/scope-mix/legacy-report.cjs | 12 ++ .../evals/fixtures/scope-mix/notes.txt | 5 + .../scope-mix/tests-e2e/checkout.spec.ts | 12 ++ .../evals/fixtures/tenant-guard.test.ts | 34 +++++ .../evals/fixtures/tenant-guard.ts | 36 ++++++ .../code-review/evals/promptfooconfig.yaml | 116 ++++++++++++++++++ .../evals/prompts/security-diff.txt | 18 +++ .../code-review/evals/prompts/security.txt | 3 +- 13 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 plugins/code-review/evals/fixtures/access-widening.diff create mode 100644 plugins/code-review/evals/fixtures/iac-exposure.tf create mode 100644 plugins/code-review/evals/fixtures/scope-mix/.github/workflows/ci.yml create mode 100644 plugins/code-review/evals/fixtures/scope-mix/build.mjs create mode 100644 plugins/code-review/evals/fixtures/scope-mix/legacy-report.cjs create mode 100644 plugins/code-review/evals/fixtures/scope-mix/notes.txt create mode 100644 plugins/code-review/evals/fixtures/scope-mix/tests-e2e/checkout.spec.ts create mode 100644 plugins/code-review/evals/fixtures/tenant-guard.test.ts create mode 100644 plugins/code-review/evals/fixtures/tenant-guard.ts create mode 100644 plugins/code-review/evals/prompts/security-diff.txt diff --git a/plugins/code-review/CHANGELOG.md b/plugins/code-review/CHANGELOG.md index 26d2d2b..f486851 100644 --- a/plugins/code-review/CHANGELOG.md +++ b/plugins/code-review/CHANGELOG.md @@ -76,6 +76,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 allowlist - `missing-access-check` calibration now routes a test that would stay green if the guard regressed to `tests` · test-fidelity, instead of grading a test gap as a security high +- Evals — a unified-diff scanner track plus four scenarios covering the defects this round + fixed: `iac-exposure` recall, `access-widening` on a diff, a guard whose weak tests must route + to `tests` · test-fidelity rather than a security high, and scope classification over a mixed + tree (`.mjs`/`.cjs`, an `e2e` directory, `.txt`, a CI workflow) ## [0.3.0] - 2026-09-02 diff --git a/plugins/code-review/evals/fixtures/access-widening.diff b/plugins/code-review/evals/fixtures/access-widening.diff new file mode 100644 index 0000000..01f3023 --- /dev/null +++ b/plugins/code-review/evals/fixtures/access-widening.diff @@ -0,0 +1,59 @@ +diff --git a/src/api/contracts.router.ts b/src/api/contracts.router.ts +index 8a1c4f2..b77e910 100644 +--- a/src/api/contracts.router.ts ++++ b/src/api/contracts.router.ts +@@ -1,24 +1,23 @@ + import { Router } from 'express' + import { requireAuth } from '../auth/middleware' + import { requireScope } from '../auth/scopes' + import { ContractStore } from '../store/contracts' + + export const contracts = Router() + const store = new ContractStore() + +-contracts.get('/contracts/:id', requireAuth, requireScope('contract:read:own'), async (req, res) => { +- const contract = await store.byId(req.params.id, { ownerId: req.user.id }) ++contracts.get('/contracts/:id', requireAuth, requireScope('org:read'), async (req, res) => { ++ const contract = await store.byId(req.params.id) + if (!contract) return res.status(404).end() + res.json(contract) + }) + +-contracts.get('/contracts/:id/export', requireAuth, async (req, res) => { ++contracts.get('/contracts/:id/export', async (req, res) => { + const rows = await store.exportRows(req.params.id) + res.type('text/csv').send(toCsv(rows)) + }) + +-contracts.post('/contracts/:id/sign', requireAuth, requireScope('contract:sign'), async (req, res) => { ++contracts.post('/contracts/:id/sign', requireAuth, requireScope('contract:sign'), requireOwner, async (req, res) => { + const signed = await store.sign(req.params.id, req.user.id) + res.status(201).json(signed) + }) +diff --git a/src/store/webhooks.ts b/src/store/webhooks.ts +index 3c0e1aa..5f2b881 100644 +--- a/src/store/webhooks.ts ++++ b/src/store/webhooks.ts +@@ -3,11 +3,11 @@ import { db } from '../db' + export class WebhookStore { + async listForTenant(tenantId: string, status: string) { + const rows = await db.query( +- 'SELECT id, url, status FROM webhooks WHERE tenant_id = $1 AND status = $2', +- [tenantId, status], ++ `SELECT id, url, status FROM webhooks WHERE status = '${status}'`, ++ [], + ) + return rows + } + } +diff --git a/config/cors.ts b/config/cors.ts +index 11a9b02..2d4e775 100644 +--- a/config/cors.ts ++++ b/config/cors.ts +@@ -1,8 +1,8 @@ + export const corsOptions = { +- origin: ['https://app.acme.com', 'https://admin.acme.com', 'https://staging.acme.com'], ++ origin: ['https://app.acme.com', 'https://admin.acme.com'], + credentials: true, + maxAge: 600, + } diff --git a/plugins/code-review/evals/fixtures/iac-exposure.tf b/plugins/code-review/evals/fixtures/iac-exposure.tf new file mode 100644 index 0000000..7c506a1 --- /dev/null +++ b/plugins/code-review/evals/fixtures/iac-exposure.tf @@ -0,0 +1,104 @@ +terraform { + required_version = ">= 1.6" +} + +variable "environment" { + type = string + default = "prod" +} + +variable "db_admin_password" { + type = string + default = "Sup3rS3cret-prod!" +} + +resource "tls_private_key" "deploy" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "local_file" "deploy_key" { + content = tls_private_key.deploy.private_key_pem + filename = "${path.module}/keys/deploy_id_rsa" +} + +output "database_admin_password" { + value = var.db_admin_password +} + +output "cluster_endpoint" { + value = google_container_cluster.primary.endpoint +} + +data "google_secret_manager_secret_version" "stripe" { + secret = "stripe-webhook-signing-key" +} + +resource "google_cloud_run_service_iam_member" "admin_invoker" { + service = google_cloud_run_service.internal_admin.name + role = "roles/run.invoker" + member = "allUsers" +} + +resource "google_storage_bucket_iam_member" "public_assets" { + bucket = google_storage_bucket.marketing_assets.name + role = "roles/storage.objectViewer" + member = "allUsers" +} + +resource "google_storage_bucket" "marketing_assets" { + name = "acme-marketing-assets" + location = "EU" +} + +resource "google_compute_firewall" "postgres" { + name = "allow-postgres" + network = google_compute_network.main.name + + allow { + protocol = "tcp" + ports = ["5432"] + } + + source_ranges = ["0.0.0.0/0"] +} + +resource "google_compute_firewall" "https" { + name = "allow-https" + network = google_compute_network.main.name + + allow { + protocol = "tcp" + ports = ["443"] + } + + source_ranges = ["0.0.0.0/0"] +} + +resource "google_iam_workload_identity_pool_provider" "github" { + workload_identity_pool_provider_id = "github" + + attribute_condition = "attribute.repository == 'acme/payments'" + + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} + +resource "google_cloud_run_service" "internal_admin" { + name = "internal-admin" + location = "europe-west1" + + template { + spec { + containers { + image = "eu.gcr.io/acme/webhooks:1.4.2" + + env { + name = "STRIPE_SIGNING_KEY" + value = data.google_secret_manager_secret_version.stripe.secret_data + } + } + } + } +} diff --git a/plugins/code-review/evals/fixtures/scope-mix/.github/workflows/ci.yml b/plugins/code-review/evals/fixtures/scope-mix/.github/workflows/ci.yml new file mode 100644 index 0000000..4a017fd --- /dev/null +++ b/plugins/code-review/evals/fixtures/scope-mix/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: ci + +on: + pull_request_target: + branches: [main] + +permissions: write-all + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci && npm test + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/plugins/code-review/evals/fixtures/scope-mix/build.mjs b/plugins/code-review/evals/fixtures/scope-mix/build.mjs new file mode 100644 index 0000000..2b48401 --- /dev/null +++ b/plugins/code-review/evals/fixtures/scope-mix/build.mjs @@ -0,0 +1,28 @@ +import { readFile, writeFile, readdir } from 'node:fs/promises' +import { join, extname } from 'node:path' + +export async function buildManifest(sourceDir, outFile) { + const entries = await readdir(sourceDir, { withFileTypes: true }) + const manifest = [] + + for (const entry of entries) { + if (!entry.isFile()) continue + if (extname(entry.name) !== '.json') continue + + const raw = await readFile(join(sourceDir, entry.name), 'utf8') + const parsed = JSON.parse(raw) + + if (parsed.expiresAt && Date.now() - parsed.expiresAt > 604800000) continue + + manifest.push({ + id: parsed.id, + title: parsed.title, + slug: parsed.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''), + tags: (parsed.tags ?? []).map((t) => t.trim().toLowerCase()).filter(Boolean), + }) + } + + manifest.sort((a, b) => a.slug.localeCompare(b.slug)) + await writeFile(outFile, JSON.stringify({ generatedAt: Date.now(), entries: manifest }, null, 2)) + return manifest.length +} diff --git a/plugins/code-review/evals/fixtures/scope-mix/legacy-report.cjs b/plugins/code-review/evals/fixtures/scope-mix/legacy-report.cjs new file mode 100644 index 0000000..9131af5 --- /dev/null +++ b/plugins/code-review/evals/fixtures/scope-mix/legacy-report.cjs @@ -0,0 +1,12 @@ +const { createWriteStream } = require('node:fs') + +function proc(rows, out) { + const s = createWriteStream(out) + s.write('id,title,slug,tags\n') + for (const r of rows) { + s.write([r.id, JSON.stringify(r.title), r.title.toLowerCase().replace(/[^a-z0-9]+/g, '-'), r.tags.join(' ')].join(',') + '\n') + } + s.end() +} + +module.exports = { proc } diff --git a/plugins/code-review/evals/fixtures/scope-mix/notes.txt b/plugins/code-review/evals/fixtures/scope-mix/notes.txt new file mode 100644 index 0000000..26b6d35 --- /dev/null +++ b/plugins/code-review/evals/fixtures/scope-mix/notes.txt @@ -0,0 +1,5 @@ +Release notes draft for the manifest builder change. + +- the manifest now skips entries whose expiry is more than a week old +- slugs are generated from the title rather than taken from the payload +- the legacy CSV reporter keeps its own copy of the slug rule for now diff --git a/plugins/code-review/evals/fixtures/scope-mix/tests-e2e/checkout.spec.ts b/plugins/code-review/evals/fixtures/scope-mix/tests-e2e/checkout.spec.ts new file mode 100644 index 0000000..c37a690 --- /dev/null +++ b/plugins/code-review/evals/fixtures/scope-mix/tests-e2e/checkout.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test' + +test('a signed-in shopper can pay for a basket', async ({ page }) => { + await page.goto('/basket') + await page.getByRole('button', { name: 'Checkout' }).click() + expect(await page.getByTestId('step').textContent()).toBe('payment') + await page.getByLabel('Card number').fill('4242424242424242') + await page.getByLabel('Expiry').fill('12/30') + expect(await page.getByRole('button', { name: 'Pay' }).isEnabled()).toBe(true) + await page.getByRole('button', { name: 'Pay' }).click() + await expect(page.getByTestId('receipt')).toBeVisible() +}) diff --git a/plugins/code-review/evals/fixtures/tenant-guard.test.ts b/plugins/code-review/evals/fixtures/tenant-guard.test.ts new file mode 100644 index 0000000..986f702 --- /dev/null +++ b/plugins/code-review/evals/fixtures/tenant-guard.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, vi } from 'vitest' +import { requireTenantMember, getInvoice } from './tenant-guard' + +vi.mock('./db', () => ({ + db: { query: vi.fn(async () => [{ id: 'inv_1', total_cents: 2500, status: 'open' }]) }, +})) + +const res = () => { + const r: any = {} + r.status = vi.fn(() => r) + r.json = vi.fn(() => r) + return r +} + +describe('requireTenantMember', () => { + it('rejects a caller from another tenant', () => { + const r = res() + const next = vi.fn() + + requireTenantMember({ session: { tenantId: 't1' }, params: { tenantId: 't1' } } as any, r, next) + + expect(next).toHaveBeenCalled() + }) +}) + +describe('getInvoice', () => { + it('does not leak an invoice belonging to another tenant', async () => { + const r = res() + + await getInvoice({ session: { tenantId: 't1' }, params: { invoiceId: 'inv_1' } } as any, r) + + expect(r.json).toHaveBeenCalled() + }) +}) diff --git a/plugins/code-review/evals/fixtures/tenant-guard.ts b/plugins/code-review/evals/fixtures/tenant-guard.ts new file mode 100644 index 0000000..87eff42 --- /dev/null +++ b/plugins/code-review/evals/fixtures/tenant-guard.ts @@ -0,0 +1,36 @@ +import { Router, type Request, type Response } from 'express' +import { db } from './db' + +export type Session = { userId: string; tenantId: string; roles: string[] } + +export function requireTenantMember(req: Request, res: Response, next: () => void) { + const session = req.session as Session | undefined + if (!session) return res.status(401).json({ error: 'unauthenticated' }) + if (session.tenantId !== req.params.tenantId) { + return res.status(403).json({ error: 'forbidden' }) + } + next() +} + +export async function listInvoices(req: Request, res: Response) { + const session = req.session as Session + const rows = await db.query( + 'SELECT id, total_cents, status FROM invoices WHERE tenant_id = $1 ORDER BY issued_at DESC LIMIT 100', + [session.tenantId], + ) + res.json(rows) +} + +export async function getInvoice(req: Request, res: Response) { + const session = req.session as Session + const [row] = await db.query( + 'SELECT id, total_cents, status FROM invoices WHERE id = $1 AND tenant_id = $2', + [req.params.invoiceId, session.tenantId], + ) + if (!row) return res.status(404).json({ error: 'not found' }) + res.json(row) +} + +export const router = Router() +router.get('/tenants/:tenantId/invoices', requireTenantMember, listInvoices) +router.get('/tenants/:tenantId/invoices/:invoiceId', requireTenantMember, getInvoice) diff --git a/plugins/code-review/evals/promptfooconfig.yaml b/plugins/code-review/evals/promptfooconfig.yaml index f326bdb..c38ce59 100644 --- a/plugins/code-review/evals/promptfooconfig.yaml +++ b/plugins/code-review/evals/promptfooconfig.yaml @@ -16,6 +16,8 @@ prompts: label: security-track - id: file://prompts/performance.txt label: performance-track + - id: file://prompts/security-diff.txt + label: security-diff-track - id: file://prompts/spec.txt label: spec-track # Standards track: quality-review with the fixture dir declared as the repository root @@ -687,3 +689,117 @@ tests: `` `standards` · · · L — "" ( ›
) → ``: a kebab-case slug, a verbatim quoted rule, and a file › section citation. No `standards` bullet lacks the quoted rule. + + # ============================================================================ + # Regression guards — the rules and the scope boundaries added from run analysis + # ============================================================================ + + - description: 'eval-16 iac-exposure-recall (iac-exposure.tf)' + prompts: [security-track] + vars: + file: plugins/code-review/evals/fixtures/iac-exposure.tf + assert: + - type: regex + value: 'iac-exposure' + - type: llm-rubric + value: >- + Flags the generated `tls_private_key.deploy` — written to remote state and to + `local_file.deploy_key` on disk — as `security` · iac-exposure · high. + - type: llm-rubric + value: >- + Flags BOTH credential leaks in the declaration: the plaintext `default` on the + `db_admin_password` variable and the `database_admin_password` output that + carries it without `sensitive`. + - type: llm-rubric + value: >- + Flags the `allUsers` invoker binding on the `internal_admin` Cloud Run service + and the `0.0.0.0/0` source range reaching port 5432 as over-wide grants + (`iac-exposure`, high). + - type: llm-rubric + value: >- + Flags the GitHub OIDC provider whose `attribute_condition` pins only + `attribute.repository` — any workflow or ref of that repository satisfies it. + - type: llm-rubric + value: >- + Does NOT flag as findings, and names on the `Not flagged` line: the `allUsers` + objectViewer binding on the marketing-assets bucket (public by design), the + `0.0.0.0/0` range on port 443 (a public port), the Stripe key read from the + secret-manager data source, and the `cluster_endpoint` output (not a + credential). + - type: llm-rubric + value: >- + Every finding is tagged `security` with a rule from the master table and + severity `high` or `medium` — no craft family (readability, naming, module, + objects, patterns, simplicity) appears at all. + + - description: 'eval-17 access-widening-diff (access-widening.diff)' + prompts: [security-diff-track] + vars: + file: plugins/code-review/evals/fixtures/access-widening.diff + assert: + - type: regex + value: 'access-widening' + - type: llm-rubric + value: >- + Flags the `/contracts/:id` route as `security` · access-widening · high: the + required scope went from `contract:read:own` to `org:read` AND the + `{ ownerId: req.user.id }` predicate was dropped from the store call. + - type: llm-rubric + value: >- + Flags the `/contracts/:id/export` route, which lost `requireAuth` entirely. + - type: llm-rubric + value: >- + Flags the webhook query losing its `tenant_id` predicate as `access-widening` + (an `injection-sink` finding for the interpolated `status` alongside it is + correct, not a substitute for it). + - type: llm-rubric + value: >- + Does NOT flag the two hunks that tighten: `requireOwner` added to the sign + route, and the CORS origin list losing `https://staging.acme.com`. Both are + named on the `Not flagged` line. + + - description: 'eval-18 guard-test-fidelity-routing (tenant-guard.ts + tenant-guard.test.ts)' + prompts: [security-track] + vars: + file: plugins/code-review/evals/fixtures/tenant-guard.ts plugins/code-review/evals/fixtures/tenant-guard.test.ts + assert: + - type: llm-rubric + value: >- + Raises NO `missing-access-check` finding against `tenant-guard.ts`: the + middleware compares the session tenant with the route tenant, and both queries + carry a `tenant_id` predicate bound as a parameter. + - type: llm-rubric + value: >- + Identifies that both tests claim a boundary their assertions never exercise + (each passes the SAME tenant and asserts only that the happy path ran), and + routes it to the `tests` family as test-fidelity — in a HANDOFF block or named + as belonging to the tests lens — rather than grading it as a `security` finding + of any severity. + - type: llm-rubric + value: >- + No finding in the output carries the `security` family with severity `high`. + + - description: 'eval-19 scope-classification (scope-mix/)' + prompts: [quality-track] + vars: + file: plugins/code-review/evals/fixtures/scope-mix + assert: + - type: llm-rubric + value: >- + `build.mjs` and `legacy-report.cjs` were reviewed as source files — the report + judges their content (findings against them, or an explicit clean verdict naming + them). Neither is listed as skipped or dismissed as tooling. + - type: llm-rubric + value: >- + `tests-e2e/checkout.spec.ts` is treated as a test file — it is reviewed, and any + finding against it comes from the `tests` family (for example the interleaved + act/assert steps as test-structure), never dismissed for sitting outside a + directory named `test`. + - type: llm-rubric + value: >- + The `Skipped` line names `.github/workflows/ci.yml` and says its triggers, + permissions and secret handling are not line-graded here, pointing the reader at + `/security-review`. `notes.txt` is listed as skipped too. + - type: regex + value: '[Ss]kipped' + diff --git a/plugins/code-review/evals/prompts/security-diff.txt b/plugins/code-review/evals/prompts/security-diff.txt new file mode 100644 index 0000000..2d9658c --- /dev/null +++ b/plugins/code-review/evals/prompts/security-diff.txt @@ -0,0 +1,18 @@ +Act as the `security` scanner of the code-review plugin. Read +`plugins/code-review/references/rules/security.md` and +`plugins/code-review/references/severity.md` completely, then read {{file}} in full — +it is a **unified diff** of the change under review, so the `-` side is what stood +before and the `+` side is what the change introduces — and judge it against the +`security` family only. The conventions note is "none" and the standards slot is +"none". The files the diff touches are not on disk; the diff is the whole evidence. + +Return findings only: no report skeleton, no headline, no tally, no edits, and nothing +written to disk. State each finding as its own markdown bullet in exactly this shape: + +`security` · rule · severity · L — → + +`L` names the diff's own file and line region (`src/api/contracts.router.ts` +around the changed hunk); a pattern alone is never a finding. After the findings add +one `Not flagged:` prose line naming each look-alike you cleared and the mitigation +that clears it — a hunk that *tightens* a boundary belongs there, never among the +findings. diff --git a/plugins/code-review/evals/prompts/security.txt b/plugins/code-review/evals/prompts/security.txt index fd926f6..662a856 100644 --- a/plugins/code-review/evals/prompts/security.txt +++ b/plugins/code-review/evals/prompts/security.txt @@ -14,4 +14,5 @@ as its own markdown bullet in exactly this shape: and which is the sink; a pattern alone is never a finding. After the findings add one `Not flagged:` prose line naming each look-alike you cleared and the mitigation that clears it, and a `CANDIDATES` block only for a confirmed pair whose mitigation is in -doubt. +doubt. A real problem that belongs to another lens's family goes in a `HANDOFF` block at +the end, naming that family and rule — never graded as a `security` finding to keep it. From aa699c109f3b9a7081851ef2b88e9500d8e1f503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 05:39:50 +0200 Subject: [PATCH 22/44] test(code-review): name the two filter literals in the folded-rules fixture --- plugins/code-review/evals/fixtures/quality-calibration-2.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/code-review/evals/fixtures/quality-calibration-2.ts b/plugins/code-review/evals/fixtures/quality-calibration-2.ts index 78fc163..b5d21e6 100644 --- a/plugins/code-review/evals/fixtures/quality-calibration-2.ts +++ b/plugins/code-review/evals/fixtures/quality-calibration-2.ts @@ -11,6 +11,8 @@ export type Order = { }; const PAGE_SIZE = 20; +const WEB_CHANNEL = "channel = 'web'"; +const PAID_STATUS = "status = 'paid'"; export class OrderDtoMapper { toDto(order: Order): OrderDto { @@ -65,7 +67,7 @@ export class OrdersController { } async list(_req: Request, res: Response): Promise { - const sql = new OrderQuery().where("channel = 'web'").where("status = 'paid'").limit(PAGE_SIZE).build(); + const sql = new OrderQuery().where(WEB_CHANNEL).where(PAID_STATUS).limit(PAGE_SIZE).build(); const orders = await this.repo.query(sql, []); res.json(orders.map((order) => this.mapper.toDto(order))); } From d4bfcdb8bd57e1a074d8d4451fdcf9a19fd92408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 05:41:45 +0200 Subject: [PATCH 23/44] test(fd3): add declared-gap, protected-path, ownerless-gap and session-file scenarios --- plugins/fd3/CHANGELOG.md | 4 + .../fixtures/gap-rollout-spec/DEFECTS.md | 40 +++ .../gap-rollout-spec/repo-a/README.md | 9 + .../services/checkout/src/api/charge.ts | 8 + .../repo-a/services/checkout/src/config.ts | 3 + .../services/ledger/migrations/README.md | 5 + .../repo-a/services/ledger/src/api/entries.ts | 10 + .../gap-rollout-spec/repo-b/README.md | 3 + .../repo-b/src/components/PaymentStatus.tsx | 8 + .../gap-rollout-spec/spec/gap-rollout-spec.md | 224 +++++++++++++++++ .../ownerless-gap-payments-spec/DEFECTS.md | 36 +++ .../ownerless-gap-payments-spec/README.md | 8 + .../ownerless-gap-payments-spec/package.json | 8 + .../spec/ownerless-gap-spec.md | 208 +++++++++++++++ .../src/billing/charge.ts | 30 +++ .../src/queue/worker.ts | 16 ++ .../src/store/idempotency.ts | 11 + .../src/webhooks/enqueue.ts | 10 + .../protected-path-rollout-spec/DEFECTS.md | 53 ++++ .../repo-a/.github/workflows/deploy.yml | 13 + .../repo-a/CODEOWNERS | 3 + .../repo-a/README.md | 9 + .../services/checkout/src/api/charge.ts | 8 + .../repo-a/services/checkout/src/config.ts | 3 + .../services/ledger/migrations/README.md | 5 + .../repo-a/services/ledger/src/api/entries.ts | 10 + .../repo-b/README.md | 3 + .../repo-b/src/components/PaymentStatus.tsx | 8 + .../spec/protected-path-spec.md | 237 ++++++++++++++++++ .../evals/lib/checks/grill-session-files.mjs | 34 +++ .../evals/lib/checks/split-declared-gap.mjs | 37 +++ .../evals/lib/checks/split-protected-path.mjs | 34 +++ plugins/fd3/evals/lib/checks/split-shared.mjs | 18 +- .../lib/checks/validate-ownerless-gap.mjs | 41 +++ plugins/fd3/evals/promptfooconfig.yaml | 28 +++ .../fd3/evals/prompts/grill-session-files.txt | 1 + .../fd3/evals/prompts/split-declared-gap.txt | 1 + .../evals/prompts/split-protected-path.txt | 1 + .../evals/prompts/validate-ownerless-gap.txt | 3 + plugins/fd3/evals/reset-sandboxes.sh | 4 + 40 files changed, 1192 insertions(+), 3 deletions(-) create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/README.md create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/api/charge.ts create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/config.ts create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/migrations/README.md create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/src/api/entries.ts create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/README.md create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/src/components/PaymentStatus.tsx create mode 100644 plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/DEFECTS.md create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/README.md create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/package.json create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/spec/ownerless-gap-spec.md create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/billing/charge.ts create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/queue/worker.ts create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/store/idempotency.ts create mode 100644 plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/webhooks/enqueue.ts create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/DEFECTS.md create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/.github/workflows/deploy.yml create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/CODEOWNERS create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/README.md create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/api/charge.ts create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/config.ts create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/migrations/README.md create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/src/api/entries.ts create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/README.md create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/src/components/PaymentStatus.tsx create mode 100644 plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md create mode 100644 plugins/fd3/evals/lib/checks/grill-session-files.mjs create mode 100644 plugins/fd3/evals/lib/checks/split-declared-gap.mjs create mode 100644 plugins/fd3/evals/lib/checks/split-protected-path.mjs create mode 100644 plugins/fd3/evals/lib/checks/validate-ownerless-gap.mjs create mode 100644 plugins/fd3/evals/prompts/grill-session-files.txt create mode 100644 plugins/fd3/evals/prompts/split-declared-gap.txt create mode 100644 plugins/fd3/evals/prompts/split-protected-path.txt create mode 100644 plugins/fd3/evals/prompts/validate-ownerless-gap.txt diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 5510371..681cdc0 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -27,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A `depends-on` edge between two tasks on the same branch must name the file or symbol they share, in the dependent task's `## Note` and in the report; an edge that cannot be named is dropped, because it only serialises the implementation stage +- Evals — four scenarios covering the defects this round fixed: a split over a spec whose + `ready` verdict carries a declared gap, a split over a `CODEOWNERS`-protected path, a + validation of an ownerless gap, and a grilling run whose two bookkeeping files must land in + `notes/` and `research/` ### Fixed diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md new file mode 100644 index 0000000..3e31d3b --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md @@ -0,0 +1,40 @@ +# gap-rollout-spec — fixture contract + +This file is fixture documentation only. `reset-sandboxes.sh` excludes it from the sandbox copy. +`rollout-spec` with one change: its last verdict line carries a blocked claim that the spec itself +declares as a gap. It serves split-declared-gap. + +## The declared gap + +Section 7 names the unmeasured ledger write ceiling, its owner (**the platform team**) and its +placement (**a gate before phase 2**). Section 12's `### Validation pass — 2026-07-30` block counts +it, so the verdict line reads: + +`Verdict: ready — claims: 1 verified / 0 deferred / 1 blocked — spec 224 lines at this verdict` + +All three halves are load-bearing. `ready` with a blocked claim is what the precondition must +accept; the owner and the placement are what make it a declared gap rather than a stop; and `224` +equals `wc -l` on the spec, so any edit to the file must be followed by rewriting the number. +Removing the owner from section 7 turns this fixture into a stop-before-step-1 case and breaks the +scenario — that case has its own fixture, `ownerless-gap-payments-spec`, on the validate side. + +## The 7-task split + +The six delivery tasks of `rollout-spec` (see that fixture's DEFECTS.md for the frozen cut, the +element→owner map and the sentinel strings, which are unchanged here) **plus one operational task** +for the gap: `repository: none`, no branch, no element code, and a `## Note` naming the platform +team and the phase-2 gate. Seven task files, exactly one of them operational. + +## Precondition material + +Unlike `rollout-spec`, the spec is read-only in this scenario: the split writes task files and the +split report beside the spec (`spec/gap-rollout-spec.split.md`) and modifies nothing. + +## Load-bearing line numbers + +Identical to `rollout-spec`: + +- `repo-a/services/checkout/src/api/charge.ts:6` — `postCharge` +- `repo-a/services/checkout/src/config.ts:2` — `asyncSettlement: false` +- `repo-a/services/ledger/src/api/entries.ts:7` — `listEntries` +- `repo-b/src/components/PaymentStatus.tsx:5` — `PaymentStatus` diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/README.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/README.md new file mode 100644 index 0000000..54e96bf --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/README.md @@ -0,0 +1,9 @@ +# commerce-core (repo-a) + +Monorepo. Two services, two owning teams: + +- `services/checkout/` — owned by team-checkout +- `services/ledger/` — owned by team-ledger + +Pull requests must be scoped to one service's subtree; CODEOWNERS requires the owning team's +approval per subtree. Branches follow `feat/-`. diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/api/charge.ts b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/api/charge.ts new file mode 100644 index 0000000..cd4b9c7 --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/api/charge.ts @@ -0,0 +1,8 @@ +export interface ChargeBody { + orderId: string; + amountMinor: number; +} + +export async function postCharge(body: ChargeBody) { + return { status: "accepted", orderId: body.orderId }; +} diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/config.ts b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/config.ts new file mode 100644 index 0000000..96692a0 --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/checkout/src/config.ts @@ -0,0 +1,3 @@ +export const flags = { + asyncSettlement: false, +}; diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/migrations/README.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/migrations/README.md new file mode 100644 index 0000000..4594af4 --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/migrations/README.md @@ -0,0 +1,5 @@ +# Ledger migrations + +SQL files in this directory are applied by CI in filename order (`NNNN_description.sql`) on merge +to `main`. A migration is irreversible once applied to the shared staging database — expand-only +changes land here; contracting changes wait for their cleanup gate. diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/src/api/entries.ts b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/src/api/entries.ts new file mode 100644 index 0000000..3312e13 --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-a/services/ledger/src/api/entries.ts @@ -0,0 +1,10 @@ +export interface LedgerEntry { + orderId: string; + amountMinor: number; + direction: "debit" | "credit"; +} + +export async function listEntries(orderId: string): Promise { + void orderId; + return []; +} diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/README.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/README.md new file mode 100644 index 0000000..ca87206 --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/README.md @@ -0,0 +1,3 @@ +# merchant-dashboard (repo-b) + +Merchant-facing web app, owned by team-web. Branches follow `feat/`. diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/src/components/PaymentStatus.tsx b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/src/components/PaymentStatus.tsx new file mode 100644 index 0000000..155abe8 --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/repo-b/src/components/PaymentStatus.tsx @@ -0,0 +1,8 @@ +export interface PaymentStatusProps { + orderId: string; +} + +export function PaymentStatus(props: PaymentStatusProps) { + void props; + return null; +} diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md new file mode 100644 index 0000000..a00d1b2 --- /dev/null +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md @@ -0,0 +1,224 @@ +# Asynchronous settlement with a merchant-visible ledger — SPEC + +**What changes:** checkout emits settlement events into a new ledger table, the ledger service +exposes them, and the merchant dashboard shows payment status — built dark in phase 1, switched on +in phase 2. + +- Epic: LED-100 +- Status: validated +- Date: 2026-07-28 + +This spec supersedes nothing; there are no companion documents. + +## 2. Problem and goal + +Settlement today is implicit: checkout accepts a charge (`repo-a/services/checkout/src/api/charge.ts:6`) +and nothing records the resulting ledger movement, so merchants cannot see payment status anywhere. +The ledger service has an entries endpoint stub that returns nothing +(`repo-a/services/ledger/src/api/entries.ts:7`), and the dashboard has an unrouted placeholder +component (`repo-b/src/components/PaymentStatus.tsx:5`). + +Goal: every accepted charge produces a ledger entry a merchant can see in the dashboard, switched +on per the rollout, with no behaviour change until phase 2. + +## 3. Design decisions + +| # | Decision | Rationale | +|---|---|---| +| D1 | **Ledger entries live in a new `ledger_entries` table owned by the ledger service** | The ledger service already owns the read path (`repo-a/services/ledger/src/api/entries.ts:7`); giving checkout its own copy would fork the source of truth. Cost accepted: checkout depends on the ledger schema landing first. | +| D2 | **Checkout emits settlement writes synchronously behind the `asyncSettlement` flag, default off** | The flag exists (`repo-a/services/checkout/src/config.ts:2`) and default-off keeps phase 1 dark; a queue would add a broker no current volume justifies. Cost accepted: a ledger write failure surfaces on the charge path once the flag is on. | +| D3 | **The dashboard reads through the ledger's `GET /ledger/entries` endpoint, never the database** | The dashboard is in another repository and team-web owns no database credentials; the endpoint is the contract. Cost accepted: a second network hop for status data. | +| D4 | **Phase 1 builds everything dark; phase 2 switches behaviour** | Both repositories can land and deploy independently with no user-visible change, then the switch is two small, reversible changes. Cost accepted: two deploys instead of one. | + +## 4. Target architecture + +### DB-1 — `ledger_entries` table (migration) + +New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql`: + +- Columns: `id BIGSERIAL PRIMARY KEY`, `order_id TEXT NOT NULL`, + `amount_minor BIGINT NOT NULL CHECK (amount_minor >= 0)`, + `direction TEXT NOT NULL CHECK (direction IN ('debit','credit'))`, + `created_at TIMESTAMPTZ NOT NULL DEFAULT now()`. +- Index on `(order_id, created_at)`. +- Errors: none at runtime — this element is schema only. +- Auth: applied by CI with the migration role (see the migrations README convention). +- Limits: expand-only; no column drops or renames in this spec. +- Migrations are applied by CI in filename order and are irreversible once applied to the shared + staging database, so this element must land on `main` before any code that writes to it. + +### API-2 — ledger entries endpoint (ledger service) + +`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`. + +- Request: `orderId` query parameter, required, non-empty string. +- Response: JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`. +- Errors: 400 on a missing or empty `orderId`; 200 with `[]` when no entries exist. +- Auth: the existing internal service token middleware; the dashboard's token is already accepted. +- Limits: response capped at 500 entries, newest first. + +### API-1 — settlement write from checkout + +`postCharge` (`repo-a/services/checkout/src/api/charge.ts:6`) gains a settlement write: when +`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry via the ledger +service's internal write endpoint. + +- Fields: `orderId`, `amountMinor` from the charge body; `direction` fixed to `credit`. +- Errors: a ledger write failure fails the charge with HTTP 502 (flag on); flag off, no write + happens and behaviour is byte-identical to today. +- Auth: the existing internal service token. +- Limits: one entry per accepted charge; no retries — the caller may retry the charge. + +### UI-1 — payment status panel (dashboard) + +`PaymentStatus` (`repo-b/src/components/PaymentStatus.tsx:5`) renders the entries for an order. + +- Fields: renders `amountMinor`, `direction`, `createdAt` per entry; empty state for `[]`. +- Errors: an API error renders the existing dashboard error banner. +- Auth: the dashboard's existing session; the panel adds no new auth surface. +- Limits: phase 1 renders from a local mock module only and stays unrouted — dark by D4. + +### CONFIG-1 — the settlement switch (checkout) + +`flags.asyncSettlement` (`repo-a/services/checkout/src/config.ts:2`) flips to `true`. + +- Fields: one boolean flag. +- Errors: none — the flag is read at module load. +- Auth: none — a code change through the normal review path. +- Limits: phase 2 only, after DB-1, API-1 and API-2 are deployed. + +### INTEGRATION-1 — dashboard wired to the live endpoint (dashboard) + +The panel swaps its mock module for the live `GET /ledger/entries` call and gets routed into the +order detail page. + +- Fields: same rendering contract as UI-1; the data source changes. +- Errors: same error banner path as UI-1. +- Auth: the dashboard's existing internal service token toward the ledger. +- Limits: phase 2 only, after API-2 is deployed and UI-1 has landed. + +### Prerequisites + +| Prerequisite | Status | +|---|---| +| CI applies ledger migrations on merge | met — the convention is documented in `repo-a/services/ledger/migrations/README.md` and CI already runs it for the existing schema | +| Internal service token shared between the three services | met — checkout and the dashboard already call the ledger with it today | + +## 5. Ownership + +| Repository / component | Owns | Apply mechanism | +|---|---|---| +| repo-a `services/checkout/` | API-1, CONFIG-1 | pull request; CODEOWNERS requires team-checkout approval; CI deploys on merge | +| repo-a `services/ledger/` | DB-1, API-2 | pull request; CODEOWNERS requires team-ledger approval; CI deploys on merge and applies migrations | +| repo-b | UI-1, INTEGRATION-1 | pull request; team-web approval; CI deploys on merge | + +repo-a is a monorepo with per-subtree CODEOWNERS: a pull request touching both `services/checkout/` +and `services/ledger/` needs both teams' approval, so changes are scoped to one subtree per pull +request. + +## 6. The change, per repository + +### repo-a — `services/ledger/` (team-ledger) + +1. **DB-1** — new: migration `0001_create_ledger_entries.sql` in + `repo-a/services/ledger/migrations/`. +2. **API-2** — changed: replace the stub in `repo-a/services/ledger/src/api/entries.ts:7-10` with + the real query and the 400 guard. + +### repo-a — `services/checkout/` (team-checkout) + +3. **API-1** — changed: settlement write in `repo-a/services/checkout/src/api/charge.ts:6-8`, + guarded by the flag. +4. **CONFIG-1** — changed: flip `asyncSettlement` to `true` in + `repo-a/services/checkout/src/config.ts:2`. + +### repo-b (team-web) + +5. **UI-1** — changed: real rendering plus a mock data module, component stays unrouted + (`repo-b/src/components/PaymentStatus.tsx:5-8`). +6. **INTEGRATION-1** — changed: swap the mock for the live endpoint call and route the panel into + the order detail page. + +## 7. Rollout + +| # | Phase | Where | Switches anything? | +|---|---|---|---| +| 1 | DB-1, API-2, API-1 (flag off), UI-1 (unrouted) land and deploy | repo-a, repo-b | no — everything is dark | +| 2 | CONFIG-1 flips the flag; INTEGRATION-1 routes the panel onto live data | repo-a, repo-b | yes — settlement writes begin and merchants see status | + +Build order within phase 1: DB-1 first (the migration must be applied before any writer or reader +ships), then API-2, then API-1; UI-1 is independent of all three. Phase 2 starts only after every +phase-1 item is deployed; within phase 2, CONFIG-1 and INTEGRATION-1 are independent of each +other. + +Single environment per repository; each phase is one deploy per repository, checkout after ledger. +Waiting period between phases: none — phase 2 starts as soon as every phase-1 item is deployed and +its verification rows pass. Phase 1 switches nothing, so there is nothing to observe between the +phases and no gate outside this spec's own verification. + +Hard dependencies: none outside this spec. + +**Declared gap — the production write ceiling of the ledger database is unconfirmed.** Nobody in +this session could establish how many settlement writes per second the shared staging instance +sustains before the ledger's connection pool saturates, and no measurement exists. Owner: the +platform team. Placement: a gate before phase 2 — the flag flip in CONFIG-1 is what puts real +write volume on the table, so the ceiling must be measured and recorded before that phase starts. +The phase-1 elements are unaffected: they land dark and write nothing. + +Rollback: phase 2 — flip the flag back and un-route the panel; the reversal is complete when no +new `ledger_entries` rows appear and the panel is unreachable. Phase 1 — revert the code merges; +the migration stays behind, unused (expand-only; removal is out of scope, LED-109). + +## 8. Verification + +- **DB-1** — probe: `psql "$LEDGER_DATABASE_URL" -c "\d ledger_entries"` lists the five columns + and the `(order_id, created_at)` index. Before the change: `did not find any relation`. +- **API-2** — probe: `curl -s "ledger.internal/ledger/entries?orderId=o_1"` returns `[]` with 200; + omitting `orderId` returns 400. Before the change both return the stub's empty 200. +- **API-1** — triggered: with the flag on in a test environment, post a charge; one `credit` row + for the order appears in `ledger_entries`. +- **UI-1** — triggered: render the panel in the dashboard's component preview against the mock + module; entries and the empty state both render. +- **CONFIG-1** — probe: `rg "asyncSettlement" repo-a/services/checkout/src/config.ts` shows + `true`. Before phase 2 it shows `false`. +- **INTEGRATION-1** — triggered: open an order with entries in the dashboard; the panel shows the + rows returned by API-2. + +Phase 1 is verified by the DB-1, API-2 probes plus the API-1 and UI-1 triggered checks; phase 2 by +the CONFIG-1 probe and the INTEGRATION-1 triggered check. + +## 9. Cleanup + +The subject has no cleanup in this spec: the migration is expand-only, and removing the mock data +module happens inside INTEGRATION-1's pull request. + +## 10. Out of scope + +- **Refunds in the ledger (a `refund` direction)** — owner: team-ledger, placement: ticket LED-108. +- **Dropping the mock-era fixtures from the dashboard test suite** — owner: team-web, placement: + ticket LED-109. + +## 11. Tickets + +LED-100 (epic), LED-108 and LED-109 exist in the tracker. No new tickets are needed; each task's +pull request cites LED-100. + +## 12. Appendix — the evidence record + +| Claim | How it was verified | +|---|---| +| Checkout accepts charges with no settlement record | `repo-a/services/checkout/src/api/charge.ts:6-8` — `postCharge` returns `accepted`, no write | +| The ledger entries endpoint is a stub | `repo-a/services/ledger/src/api/entries.ts:7-10` — `listEntries` returns `[]` unconditionally | +| The settlement flag exists and is off | `repo-a/services/checkout/src/config.ts:2` — `asyncSettlement: false` | +| The dashboard panel exists and is unrouted | `repo-b/src/components/PaymentStatus.tsx:5` — component returns `null`; no route references it | +| Migrations are applied by CI in filename order and are irreversible on staging | `repo-a/services/ledger/migrations/README.md` — the convention paragraph | + +### Validation pass — 2026-07-30 + +Verdict: ready — claims: 1 verified / 0 deferred / 1 blocked — spec 224 lines at this verdict + +| Claim | How it was verified | +|---|---| +| All 12 spec-level checks pass | `fd3:validate-spec` run of 2026-07-30 — every check row `pass`, no blocking findings | +| The ledger write ceiling is unmeasured | no measurement exists; declared as a gap in section 7 with the platform team as owner and a gate before phase 2 as placement | +| Verdict | phase 1: yes; phase 2: yes, behind the declared gap's gate — spec is ready to split | diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/DEFECTS.md b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/DEFECTS.md new file mode 100644 index 0000000..51345ef --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/DEFECTS.md @@ -0,0 +1,36 @@ +# ownerless-gap-payments-spec — fixture contract + +This file is fixture documentation only. `reset-sandboxes.sh` excludes it from the sandbox copy. +`gap-payments-spec` with the gap's owner and placement removed. It serves validate-ownerless-gap. + +## The unowned gap + +The same fact is unconfirmed — the platform egress rate-limit ceiling behind D5, CONFIG-1 and the +phase-2 gate — but the spec places it nowhere and names nobody: + +- section 4's sub-heading reads **`### Open question`**, not `### Declared gap`, and its paragraph + says nobody could name who sets the ceiling or who would confirm it; +- section 7's `Gate after?` cell and hard-dependency line say phase 2 waits on *a confirmation + nobody owns*; +- the section 12 evidence row marks it the same way. + +Restore an owner or a placement in any of the three and the claim becomes `deferred` on sight, which +is the other fixture's scenario (`gap-payments-spec`), not this one. + +The validate-ownerless-gap assertions require: + +- the claim reaches the report — under `## Blocked`, or under `## Deferred` only with an owner and a + placement that an answer supplied, never a stand-in like "nobody" or "TBD"; +- a `## Blocked` claim lowers the verdict to `not ready`; +- the owned out-of-scope items of section 10 (refund webhooks, invoice PDF rendering, the wiring) + never turn up as blocked — they name a team and a ticket. + +With `ask_user_question: first_option` step 4's ownership question is auto-answered, so both +outcomes are within contract; what is not is a deferred entry with no owner behind it. + +## Everything else + +Unchanged from `gap-payments-spec` — read that fixture's DEFECTS.md for the tree's facts the spec +must keep declaring (no caller for `deliver`, `post` returns `true` unconditionally, no HTTP surface, +no manifest), the risks-accepted table, the five-column rollout table and the `Limits:` bullet that +keeps section 9 consistent. Source files are byte-identical to `defective-payments-spec`'s. diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/README.md b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/README.md new file mode 100644 index 0000000..1fe4ce4 --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/README.md @@ -0,0 +1,8 @@ +# payments-service + +Charges cards and delivers `charge.settled` webhooks to merchants. + +- `src/billing/` — charge entry point and idempotency handling +- `src/webhooks/` — event enqueueing +- `src/queue/` — the delivery worker +- `src/store/` — idempotency key storage (in-memory today) diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/package.json b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/package.json new file mode 100644 index 0000000..6f439aa --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/package.json @@ -0,0 +1,8 @@ +{ + "name": "payments-service", + "version": "1.4.2", + "private": true, + "scripts": { + "test": "vitest run" + } +} diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/spec/ownerless-gap-spec.md b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/spec/ownerless-gap-spec.md new file mode 100644 index 0000000..6554828 --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/spec/ownerless-gap-spec.md @@ -0,0 +1,208 @@ +# Webhook delivery hardening — SPEC + +**What changes:** charge processing becomes durably idempotent, `charge.settled` webhook delivery +gains outcome metrics, and the rate at which the process issues delivery attempts gains a +configurable cap that is raised once the platform egress ceiling allows. + +- Ticket: PAY-231 +- Status: ready for validation +- Date: 2026-07-25 + +This spec supersedes nothing; there are no companion documents. + +## 2. Problem and goal + +Idempotency keys live in process memory (`src/store/idempotency.ts:3`), so a restart forgets every +processed order and a redelivered request charges the card twice. Webhook delivery retries exist +(`src/queue/worker.ts:3`) but nothing records delivery outcomes, so a failed delivery is invisible. +The retry loop also issues its attempts back to back with no delay (`src/queue/worker.ts:6`), and +nothing coordinates concurrent `deliver` calls, so nothing in the module bounds the attempts it +sends. The module is not wired in yet either — `deliver` has no caller and nothing imports +`src/queue/worker.ts` — so the ceiling has to be in place before it is, not after (wiring it is out +of scope; section 10). + +Goal: a redelivered charge request never charges twice across restarts, every delivery the worker +attempts is counted, and the attempts it issues stay under a ceiling that can be raised without a +code change. + +## 3. Design decisions + +| # | Decision | Rationale | +|---|---|---| +| D1 | **Charge processing stays idempotent per `orderId`** | The lookup-before-charge shape already exists (`src/billing/charge.ts:16`); this spec only makes the store durable. Cost accepted: one storage dependency where today there is none. | +| D2 | **Idempotency keys are stored in Postgres** | The service already holds a `DATABASE_URL` and the orders schema lives there; Redis would add a second stateful dependency for the same guarantee. Cost accepted: key reads join the existing database's load. | +| D3 | **Delivery retries stay capped at 5 attempts** | The cap already exists (`src/queue/worker.ts:3`) and no incident has needed more. Raising it would only delay surfacing a dead merchant endpoint. | +| D4 | **Webhook processing stays queued, off the request path** | Delivery lives in its own module (`src/queue/worker.ts:6`) and `charge` only enqueues (`src/billing/charge.ts:21`); moving delivery into the request path would put merchant endpoint latency on the charge response. Cost accepted: the merchant learns the outcome asynchronously. | +| D5 | **One process-wide cap on attempts per second, set by config and raised in phase 2** | The retry loop applies no delay (`src/queue/worker.ts:6`) and concurrent `deliver` calls do not see each other, so a per-event delay would not bound what the process sends; the cap has to be shared module state. A config value moves it without a deploy of new code. `8` per second is the largest value that stays safely under the lowest egress ceiling anyone has quoted, pending a confirmation nobody owns (the open question in section 4). Cost accepted: `1` is slower than the module's uncapped behaviour, and the whole cap reaches only the test suite until the worker is wired in (section 10). | +| D6 | **Delivery outcomes are counted in the process, not scraped** | The repository holds four modules and no HTTP surface, so a scraped endpoint would be a second change with its own contract; an exported counter is verifiable by the test runner `package.json` already names. Cost accepted: the counts are per process and are lost on restart. | +| D7 | **The cap lands before the worker is wired in** | `deliver` has no caller today, so a cap added afterwards would ship a window in which delivery runs uncapped. Cost accepted: both phases are exercised by the test suite rather than by production traffic, so phase 2 opens on a confirmation nobody owns and not on a reading. | + +**Risks accepted** + +| Risk | What it costs if it lands | Mitigation | +|---|---|---| +| `idempotency_keys` grows without bound — nothing in this spec deletes rows, and section 9 adds no cleanup. | Table size grows with order volume, and index maintenance cost rises with it. | One row per order and the primary key as the only index; a retention policy is a later change, not a blocker for this spec. | +| A provider charge that succeeds and whose key write then fails leaves the card charged with no key stored, so a redelivery charges twice. D1 keeps the existing lookup-before-charge order (`src/billing/charge.ts:16`). | One duplicate charge per occurrence, refunded by hand. | The write is retried once and then rethrown as `IdempotencyWriteFailed` rather than swallowed, so the window is visible when it opens. Closing it entirely needs reserve-before-charge, which this spec does not do. | +| Phase 1 caps the module at `1` attempt per second, below its uncapped behaviour, and phase 2 cannot start until the ceiling is confirmed — and the spec names nobody who would confirm it (the open question in section 4). | Whenever the worker is wired in (section 10), a burst takes longer to deliver for as long as the cap sits at `1`; merchants learn outcomes later. | Retries stay capped at 5 attempts (D3), so a slow burst still terminates, and phase 2 raises the cap as soon as the confirmation lands — which is a configuration change, not a release. | +| OBSERVABILITY-1's counts live in process memory (D6) and reset on every restart. | A restart during an incident loses the delivery history up to that point. | The counts are a rate signal, not a ledger: what phase 2's criterion reads is the slope over seconds, which survives any restart that is not mid-burst. | + +## 4. Target architecture + +### DB-1 — durable idempotency key store + +Replaces the in-memory `Map` in `src/store/idempotency.ts`. Contract: + +- Table `idempotency_keys` with columns `order_id TEXT PRIMARY KEY`, `charge_id TEXT NOT NULL`, + `status TEXT NOT NULL`, `created_at TIMESTAMPTZ NOT NULL DEFAULT now()`. +- Reads and writes go through the existing `getIdempotencyKey` / `saveIdempotencyKey` functions; + their signatures do not change. +- Errors: a store read failure throws `IdempotencyStoreUnavailable` out of `getIdempotencyKey`; a + write failure after a successful provider charge is retried once, then logged and rethrown as + `IdempotencyWriteFailed`. Neither is caught inside `charge` (`src/billing/charge.ts:15`), which + has no error path today and gains none here. +- Auth: the service's existing database credentials; no new principal. +- Dependencies: the `pg` client and `node-pg-migrate` for the migration, both new — + `package.json` declares no dependencies today. +- Limits: none beyond the primary key. Rows accumulate; nothing in this spec deletes them. + +### OBSERVABILITY-1 — delivery metrics + +Counter `webhook_delivery_attempts_total`, held in module scope in `src/queue/worker.ts`, +incremented by `deliver` (`src/queue/worker.ts:5`) after each attempt under the outcome that +attempt had, and read through a new exported `deliveryMetrics()`. + +- Fields: two counts, `delivered` and `failed`, returned as one object. +- Errors: incrementing cannot fail; it is an in-process integer add. +- Auth: none — in-process, no new principal. +- Limits: the counts are per process and start at zero on every restart (D6). Only `delivered` is + exercisable here — `post` (`src/queue/worker.ts:14`) returns `true` unconditionally, so nothing in + this repository can produce a failed attempt; a transport that can fail is out of scope + (section 10). + +### CONFIG-1 — delivery attempt rate cap + +New config value `DELIVERY_RATE_LIMIT`: the maximum delivery attempts per second the whole process +issues. One token bucket in module scope in `src/queue/worker.ts`, refilled at that rate and shared +by every `deliver` call; each attempt in the retry loop (`src/queue/worker.ts:6`) takes a token +first and waits when the bucket is empty. `DELIVERY_RATE_LIMIT` is read from `process.env` when the +module loads and held for the life of the process. + +- Fields: one integer environment variable, attempts per second. Default `1`, the phase-1 value. + The bucket holds one token, so there is no burst allowance above the rate. +- Errors: the same module-load read throws `DeliveryRateLimitInvalid` on a value that is not an + integer, is below `1`, or is above `8` — never a silent fallback and never a clamp. The error + carries the rejected value and the bound it broke. +- Auth: none — the environment the service runs under. +- Limits: `8` is the ceiling the read enforces, and phase 2 is what sets the value to it. + +### Open question + +**The platform egress rate-limit ceiling is unconfirmed.** The value `8` in D5 and CONFIG-1 rests +on the lowest quoted ceiling, not on a confirmed number. Nobody could say who sets that ceiling or +who would confirm it, and the spec places the question nowhere. + +### Prerequisites + +| Prerequisite | Status | +|---|---| +| Postgres reachable from the service, with a `DATABASE_URL` in the deploy manifest | asserted by the payments team; not verifiable from this repository, which holds no manifest — owner: payments team, placement: confirmed before the phase-1 deploy | +| Somewhere to set `DELIVERY_RATE_LIMIT` — the service's environment configuration lives outside this repository, which holds no manifest and no CI configuration | asserted by the payments team — owner: payments team, placement: confirmed before the phase-1 deploy, since phase 2 is a change to that configuration and nothing else | + +## 5. Ownership + +| Repository / component | Owns | Apply mechanism | +|---|---|---| +| payments-service (this repository) | everything in this spec | pull request, CI deploy on merge to `main` | + +All paths in this spec are owned by the payments team; review is one approval from that team, and +CI applies the deploy — no human runs anything by hand. + +## 6. The change, per repository + +### payments-service + +1. **DB-1** — new: migration adding `idempotency_keys`, plus rewiring `getIdempotencyKey` / + `saveIdempotencyKey` (`src/store/idempotency.ts:5`, `src/store/idempotency.ts:9`) to the table. +2. **OBSERVABILITY-1** — new: module-scope counts in `src/queue/worker.ts`, incremented inside the + retry loop (`src/queue/worker.ts:6`) once per attempt, plus the exported `deliveryMetrics()`. +3. **CONFIG-1** — new: the module-scope token bucket in `src/queue/worker.ts`, read from + `process.env` at module load, with the retry loop (`src/queue/worker.ts:6`) taking a token per + attempt. +4. **Dependencies** — `package.json` declares none today: `pg` and `node-pg-migrate` for DB-1, and + `vitest` as a dev dependency for the checks in section 8, which the existing `test` script + already invokes. + +Items 1–3 are independent — none reads anything another introduces — so they may land in any order +within the phase; item 4 lands with or before whichever of them needs it. + +## 7. Rollout + +| # | Phase | Where | Switches anything? | Gate after? | +|---|---|---|---|---| +| 1 | All four work items land and deploy together, `DELIVERY_RATE_LIMIT=1` | payments-service | yes — the store becomes durable, the counts start, and the process is capped at 1 attempt per second | yes — phase 2 waits on a ceiling confirmation nobody owns, so phase 1 closes its landing unit: one branch, one pull request | +| 2 | Set `DELIVERY_RATE_LIMIT=8` in the service's environment configuration, which lives outside this repository (section 4 prerequisites) | payments-service | yes — the cap rises to 8 attempts per second | yes — the final phase, so it closes its unit | + +Single environment; each phase is one deploy. Phase 2 opens on a confirmation nobody owns and +nothing else: no production reading gates it, because until the worker is wired in (section 10) the +counts move only under the test suite. + +Hard dependency: **an unowned confirmation of the egress rate-limit ceiling gates phase +2** (the open question in section 4). The gate is a confirmation, not a merge or a deploy. + +Rollback: phase 1 — revert the merge commit and redeploy; the `idempotency_keys` table stays +behind, unused. Phase 2 — set `DELIVERY_RATE_LIMIT` back to `1` and redeploy; the reversal is complete when that +deploy is live. There is no runtime reading to wait for: nothing calls `deliver` in the deployed +process until the worker is wired in (section 10), so the counts stay at zero either way. + +## 8. Verification + +- **DB-1** — probe: `psql "$DATABASE_URL" -c "\d idempotency_keys"` lists the four columns. Before + the change the same command errors with `did not find any relation`. +- **OBSERVABILITY-1** — triggered, through `npm test` (`vitest run`, the script `package.json` + defines): call `deliver` on two events, then assert `deliveryMetrics()` returns `delivered: 2`. + Before the change the export does not exist. The `failed` count is not exercised — nothing here + can make a delivery fail (section 10). +- **CONFIG-1** — triggered, through the same test run: importing the module with + `DELIVERY_RATE_LIMIT=abc` throws `DeliveryRateLimitInvalid`; with `DELIVERY_RATE_LIMIT=1`, three events + delivered concurrently take at least two seconds for their three attempts, which is what a + per-event delay would not produce — each of those events costs one attempt, so only a shared + bucket can space them. + +Phase 1 is verified when the DB-1 probe and the two triggered checks above pass; phase 2 by the same +suite delivering 100 events at up to 8 attempts per second, where phase 1 held the same burst to 1. + +## 9. Cleanup + +The subject has no cleanup: nothing is deleted, and the only irreversible artifact (the +`idempotency_keys` table) is additive. + +## 10. Out of scope + +- **Wiring the worker into the service** — `deliver` has no caller and nothing imports + `src/queue/worker.ts`; owner: payments team, placement: ticket PAY-262. +- **A delivery transport that can fail** — `post` (`src/queue/worker.ts:14`) returns `true` + unconditionally, so no delivery can fail today; owner: payments team, placement: ticket PAY-262, + alongside the wiring that makes it reachable. +- **Refund webhooks** — owner: payments team, placement: ticket PAY-244. +- **Invoice PDF rendering** — owner: billing team, placement: ticket PAY-251. + +## 11. Tickets + +PAY-231 exists and tracks this spec. PAY-244, PAY-251 and PAY-262 exist and hold the four +exclusions. No new tickets are needed. + +## 12. Appendix — the evidence record + +| Claim | How it was verified | +|---|---| +| Idempotency keys are in-memory today | `src/store/idempotency.ts:3` — `const keys = new Map<...>()` | +| Charge processing checks the key before charging | `src/billing/charge.ts:16` — `getIdempotencyKey` called before `callProvider` | +| Delivery retries cap at 5 attempts | `src/queue/worker.ts:3` — `MAX_DELIVERY_ATTEMPTS = 5` | +| The retry loop applies no delay between attempts, and nothing bounds concurrent calls | `src/queue/worker.ts:6` — the `for` loop awaits `post` and retries immediately; no timer, no backoff and no module-scope state in the file | +| The repository holds no HTTP surface, no metrics endpoint and no environment read | `git ls-files` returns 7 files; no server, route, `/metrics` handler or `process.env` access anywhere under `src/` | +| `package.json` declares no dependencies | `package.json` — `scripts.test` only; no `dependencies` and no `devDependencies` block | +| Nothing calls `deliver` and nothing imports the worker module | No `import` of `src/queue/worker` anywhere under `src/`; `deliver` (`src/queue/worker.ts:5`) is exported and unreferenced | +| No delivery can fail today | `src/queue/worker.ts:14` — `post` ignores its argument and returns `true`; it is module-private with no injection seam | +| Webhook enqueueing already exists | `src/webhooks/enqueue.ts:8` — `enqueueWebhook` | +| The store functions are the only key readers/writers | `grep -rn "getIdempotencyKey\|saveIdempotencyKey" src/` — hits only in `src/store/idempotency.ts` and `src/billing/charge.ts` | +| The egress rate-limit ceiling supports 8 attempts per second | Unconfirmed — no documentation states the ceiling, and nobody could be named who would confirm it | diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/billing/charge.ts b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/billing/charge.ts new file mode 100644 index 0000000..ba0bbfe --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/billing/charge.ts @@ -0,0 +1,30 @@ +import { getIdempotencyKey, saveIdempotencyKey } from "../store/idempotency"; +import { enqueueWebhook } from "../webhooks/enqueue"; + +export interface ChargeRequest { + orderId: string; + amountMinor: number; + currency: string; +} + +export interface ChargeResult { + chargeId: string; + status: "succeeded" | "declined"; +} + +export async function charge(req: ChargeRequest): Promise { + const existing = await getIdempotencyKey(req.orderId); + if (existing) { + return existing.result; + } + const result = await callProvider(req); + await saveIdempotencyKey(req.orderId, result); + await enqueueWebhook("charge.settled", result); + return result; +} + +async function callProvider(req: ChargeRequest): Promise { + const providerTimeoutMs = 8000; + void providerTimeoutMs; + return { chargeId: `ch_${req.orderId}`, status: "succeeded" }; +} diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/queue/worker.ts b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/queue/worker.ts new file mode 100644 index 0000000..fe0fb22 --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/queue/worker.ts @@ -0,0 +1,16 @@ +import type { WebhookEvent } from "../webhooks/enqueue"; + +const MAX_DELIVERY_ATTEMPTS = 5; + +export async function deliver(event: WebhookEvent): Promise { + for (let attempt = 1; attempt <= MAX_DELIVERY_ATTEMPTS; attempt += 1) { + const delivered = await post(event); + if (delivered) { + return; + } + } +} + +async function post(_event: WebhookEvent): Promise { + return true; +} diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/store/idempotency.ts b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/store/idempotency.ts new file mode 100644 index 0000000..bdff893 --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/store/idempotency.ts @@ -0,0 +1,11 @@ +import type { ChargeResult } from "../billing/charge"; + +const keys = new Map(); + +export async function getIdempotencyKey(orderId: string) { + return keys.get(orderId); +} + +export async function saveIdempotencyKey(orderId: string, result: ChargeResult) { + keys.set(orderId, { result }); +} diff --git a/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/webhooks/enqueue.ts b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/webhooks/enqueue.ts new file mode 100644 index 0000000..2bf2336 --- /dev/null +++ b/plugins/fd3/evals/fixtures/ownerless-gap-payments-spec/src/webhooks/enqueue.ts @@ -0,0 +1,10 @@ +export interface WebhookEvent { + type: string; + payload: unknown; +} + +const queue: WebhookEvent[] = []; + +export async function enqueueWebhook(type: string, payload: unknown): Promise { + queue.push({ type, payload }); +} diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/DEFECTS.md b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/DEFECTS.md new file mode 100644 index 0000000..99ad02c --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/DEFECTS.md @@ -0,0 +1,53 @@ +# protected-path-rollout-spec — fixture contract + +This file is fixture documentation only. `reset-sandboxes.sh` excludes it from the sandbox copy. +`rollout-spec` plus a seventh element whose path is owned by a team none of the others can stand +in for. It serves split-protected-path. + +## The protected path + +`repo-a/CODEOWNERS` gives `/.github/workflows/` to `@acme/release-team`, and section 5's ownership +table repeats it with the clause that makes it load-bearing: *which no other team can give*. The +new element **CI-1 — migration step in the deploy workflow** edits +`repo-a/.github/workflows/deploy.yml`, which that line covers. + +Delete either the CODEOWNERS line or the ownership row and the cut has nothing to read: the split +then legitimately folds CI-1 into a phase-1 task with the ledger work, and the assert fails on a +split that is within contract. + +## The 7-task split + +The six delivery tasks of `rollout-spec` (see that fixture's DEFECTS.md for the frozen cut, the +element→owner map and the sentinel strings, which are unchanged here) **plus CI-1**, which must: + +- carry CI-1 and nothing else; +- be a delivery task — `repository: repo-a`, never `repository: none`; +- sit in phase 1, where the migration it runs lands; +- come **before DB-1**: section 7 opens phase 1 with "CI-1 first (the migration step must exist + before a migration relies on it)", so the migration task carries exactly one `depends-on` edge + and it points at CI-1. This is the one place this fixture departs from `rollout-spec`, where + DB-1 is the root — hence `checkBoundaries(..., { precedesDb: 'CI-1' })`; +- hold its branch **alone**: no other task may name the same branch, so release-team's approval + gates one pull request rather than the whole phase-1 landing unit. + +Seven task files, no operational task. + +## Precondition material + +Section 12's `### Validation pass — 2026-07-30` block opens with + +`Verdict: ready — claims: 1 verified / 0 deferred / 0 blocked — spec 237 lines at this verdict` + +`237` equals `wc -l` on the spec; any edit to the file must be followed by rewriting the number. +The spec is read-only in this scenario: the split writes task files and the split report beside the +spec (`spec/protected-path-spec.split.md`) and modifies nothing. + +## Load-bearing line numbers + +`rollout-spec`'s four, plus the workflow CI-1 edits: + +- `repo-a/services/checkout/src/api/charge.ts:6` — `postCharge` +- `repo-a/services/checkout/src/config.ts:2` — `asyncSettlement: false` +- `repo-a/services/ledger/src/api/entries.ts:7` — `listEntries` +- `repo-b/src/components/PaymentStatus.tsx:5` — `PaymentStatus` +- `repo-a/.github/workflows/deploy.yml` — checkout then deploy, with no step between them diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/.github/workflows/deploy.yml b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/.github/workflows/deploy.yml new file mode 100644 index 0000000..1017121 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/.github/workflows/deploy.yml @@ -0,0 +1,13 @@ +name: deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Deploy services + run: ./scripts/deploy.sh diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/CODEOWNERS b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/CODEOWNERS new file mode 100644 index 0000000..6b237a9 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/CODEOWNERS @@ -0,0 +1,3 @@ +/services/ledger/ @acme/team-ledger +/services/checkout/ @acme/team-checkout +/.github/workflows/ @acme/release-team diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/README.md b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/README.md new file mode 100644 index 0000000..54e96bf --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/README.md @@ -0,0 +1,9 @@ +# commerce-core (repo-a) + +Monorepo. Two services, two owning teams: + +- `services/checkout/` — owned by team-checkout +- `services/ledger/` — owned by team-ledger + +Pull requests must be scoped to one service's subtree; CODEOWNERS requires the owning team's +approval per subtree. Branches follow `feat/-`. diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/api/charge.ts b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/api/charge.ts new file mode 100644 index 0000000..cd4b9c7 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/api/charge.ts @@ -0,0 +1,8 @@ +export interface ChargeBody { + orderId: string; + amountMinor: number; +} + +export async function postCharge(body: ChargeBody) { + return { status: "accepted", orderId: body.orderId }; +} diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/config.ts b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/config.ts new file mode 100644 index 0000000..96692a0 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/checkout/src/config.ts @@ -0,0 +1,3 @@ +export const flags = { + asyncSettlement: false, +}; diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/migrations/README.md b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/migrations/README.md new file mode 100644 index 0000000..4594af4 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/migrations/README.md @@ -0,0 +1,5 @@ +# Ledger migrations + +SQL files in this directory are applied by CI in filename order (`NNNN_description.sql`) on merge +to `main`. A migration is irreversible once applied to the shared staging database — expand-only +changes land here; contracting changes wait for their cleanup gate. diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/src/api/entries.ts b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/src/api/entries.ts new file mode 100644 index 0000000..3312e13 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-a/services/ledger/src/api/entries.ts @@ -0,0 +1,10 @@ +export interface LedgerEntry { + orderId: string; + amountMinor: number; + direction: "debit" | "credit"; +} + +export async function listEntries(orderId: string): Promise { + void orderId; + return []; +} diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/README.md b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/README.md new file mode 100644 index 0000000..ca87206 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/README.md @@ -0,0 +1,3 @@ +# merchant-dashboard (repo-b) + +Merchant-facing web app, owned by team-web. Branches follow `feat/`. diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/src/components/PaymentStatus.tsx b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/src/components/PaymentStatus.tsx new file mode 100644 index 0000000..155abe8 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/repo-b/src/components/PaymentStatus.tsx @@ -0,0 +1,8 @@ +export interface PaymentStatusProps { + orderId: string; +} + +export function PaymentStatus(props: PaymentStatusProps) { + void props; + return null; +} diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md new file mode 100644 index 0000000..a546b26 --- /dev/null +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md @@ -0,0 +1,237 @@ +# Asynchronous settlement with a merchant-visible ledger — SPEC + +**What changes:** checkout emits settlement events into a new ledger table, the ledger service +exposes them, and the merchant dashboard shows payment status — built dark in phase 1, switched on +in phase 2. + +- Epic: LED-100 +- Status: validated +- Date: 2026-07-28 + +This spec supersedes nothing; there are no companion documents. + +## 2. Problem and goal + +Settlement today is implicit: checkout accepts a charge (`repo-a/services/checkout/src/api/charge.ts:6`) +and nothing records the resulting ledger movement, so merchants cannot see payment status anywhere. +The ledger service has an entries endpoint stub that returns nothing +(`repo-a/services/ledger/src/api/entries.ts:7`), and the dashboard has an unrouted placeholder +component (`repo-b/src/components/PaymentStatus.tsx:5`). + +Goal: every accepted charge produces a ledger entry a merchant can see in the dashboard, switched +on per the rollout, with no behaviour change until phase 2. + +## 3. Design decisions + +| # | Decision | Rationale | +|---|---|---| +| D1 | **Ledger entries live in a new `ledger_entries` table owned by the ledger service** | The ledger service already owns the read path (`repo-a/services/ledger/src/api/entries.ts:7`); giving checkout its own copy would fork the source of truth. Cost accepted: checkout depends on the ledger schema landing first. | +| D2 | **Checkout emits settlement writes synchronously behind the `asyncSettlement` flag, default off** | The flag exists (`repo-a/services/checkout/src/config.ts:2`) and default-off keeps phase 1 dark; a queue would add a broker no current volume justifies. Cost accepted: a ledger write failure surfaces on the charge path once the flag is on. | +| D3 | **The dashboard reads through the ledger's `GET /ledger/entries` endpoint, never the database** | The dashboard is in another repository and team-web owns no database credentials; the endpoint is the contract. Cost accepted: a second network hop for status data. | +| D4 | **Phase 1 builds everything dark; phase 2 switches behaviour** | Both repositories can land and deploy independently with no user-visible change, then the switch is two small, reversible changes. Cost accepted: two deploys instead of one. | + +## 4. Target architecture + +### DB-1 — `ledger_entries` table (migration) + +New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql`: + +- Columns: `id BIGSERIAL PRIMARY KEY`, `order_id TEXT NOT NULL`, + `amount_minor BIGINT NOT NULL CHECK (amount_minor >= 0)`, + `direction TEXT NOT NULL CHECK (direction IN ('debit','credit'))`, + `created_at TIMESTAMPTZ NOT NULL DEFAULT now()`. +- Index on `(order_id, created_at)`. +- Errors: none at runtime — this element is schema only. +- Auth: applied by CI with the migration role (see the migrations README convention). +- Limits: expand-only; no column drops or renames in this spec. +- Migrations are applied by CI in filename order and are irreversible once applied to the shared + staging database, so this element must land on `main` before any code that writes to it. + +### API-2 — ledger entries endpoint (ledger service) + +`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`. + +- Request: `orderId` query parameter, required, non-empty string. +- Response: JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`. +- Errors: 400 on a missing or empty `orderId`; 200 with `[]` when no entries exist. +- Auth: the existing internal service token middleware; the dashboard's token is already accepted. +- Limits: response capped at 500 entries, newest first. + +### CI-1 — migration step in the deploy workflow + +`repo-a/.github/workflows/deploy.yml` gains a step that applies pending ledger migrations before +the service deploy step, so DB-1 reaches staging by the documented CI convention rather than by +hand. + +- Request/response: none — this element is a workflow definition. +- Errors: a failing migration step fails the deploy job, and no service is deployed. +- Auth: the workflow's existing deploy credentials; no new secret is introduced. +- Limits: the step runs only on `main`, in filename order, and never rolls back. + +### API-1 — settlement write from checkout + +`postCharge` (`repo-a/services/checkout/src/api/charge.ts:6`) gains a settlement write: when +`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry via the ledger +service's internal write endpoint. + +- Fields: `orderId`, `amountMinor` from the charge body; `direction` fixed to `credit`. +- Errors: a ledger write failure fails the charge with HTTP 502 (flag on); flag off, no write + happens and behaviour is byte-identical to today. +- Auth: the existing internal service token. +- Limits: one entry per accepted charge; no retries — the caller may retry the charge. + +### UI-1 — payment status panel (dashboard) + +`PaymentStatus` (`repo-b/src/components/PaymentStatus.tsx:5`) renders the entries for an order. + +- Fields: renders `amountMinor`, `direction`, `createdAt` per entry; empty state for `[]`. +- Errors: an API error renders the existing dashboard error banner. +- Auth: the dashboard's existing session; the panel adds no new auth surface. +- Limits: phase 1 renders from a local mock module only and stays unrouted — dark by D4. + +### CONFIG-1 — the settlement switch (checkout) + +`flags.asyncSettlement` (`repo-a/services/checkout/src/config.ts:2`) flips to `true`. + +- Fields: one boolean flag. +- Errors: none — the flag is read at module load. +- Auth: none — a code change through the normal review path. +- Limits: phase 2 only, after DB-1, API-1 and API-2 are deployed. + +### INTEGRATION-1 — dashboard wired to the live endpoint (dashboard) + +The panel swaps its mock module for the live `GET /ledger/entries` call and gets routed into the +order detail page. + +- Fields: same rendering contract as UI-1; the data source changes. +- Errors: same error banner path as UI-1. +- Auth: the dashboard's existing internal service token toward the ledger. +- Limits: phase 2 only, after API-2 is deployed and UI-1 has landed. + +### Prerequisites + +| Prerequisite | Status | +|---|---| +| CI applies ledger migrations on merge | met — the convention is documented in `repo-a/services/ledger/migrations/README.md` and CI already runs it for the existing schema | +| Internal service token shared between the three services | met — checkout and the dashboard already call the ledger with it today | + +## 5. Ownership + +| Repository / component | Owns | Apply mechanism | +|---|---|---| +| repo-a `services/checkout/` | API-1, CONFIG-1 | pull request; CODEOWNERS requires team-checkout approval; CI deploys on merge | +| repo-a `services/ledger/` | DB-1, API-2 | pull request; CODEOWNERS requires team-ledger approval; CI deploys on merge and applies migrations | +| repo-a `.github/workflows/` | CI-1 | pull request; CODEOWNERS requires release-team approval, which no other team can give | +| repo-b | UI-1, INTEGRATION-1 | pull request; team-web approval; CI deploys on merge | + +repo-a is a monorepo with per-subtree CODEOWNERS: a pull request touching both `services/checkout/` +and `services/ledger/` needs both teams' approval, so changes are scoped to one subtree per pull +request. + +## 6. The change, per repository + +### repo-a — `services/ledger/` (team-ledger) + +1. **DB-1** — new: migration `0001_create_ledger_entries.sql` in + `repo-a/services/ledger/migrations/`. +2. **API-2** — changed: replace the stub in `repo-a/services/ledger/src/api/entries.ts:7-10` with + the real query and the 400 guard. + +### repo-a — `.github/workflows/` (release-team) + +3. **CI-1** — changed: add the migration step to `repo-a/.github/workflows/deploy.yml` ahead of the + service deploy step. + +### repo-a — `services/checkout/` (team-checkout) + +4. **API-1** — changed: settlement write in `repo-a/services/checkout/src/api/charge.ts:6-8`, + guarded by the flag. +5. **CONFIG-1** — changed: flip `asyncSettlement` to `true` in + `repo-a/services/checkout/src/config.ts:2`. + +### repo-b (team-web) + +6. **UI-1** — changed: real rendering plus a mock data module, component stays unrouted + (`repo-b/src/components/PaymentStatus.tsx:5-8`). +7. **INTEGRATION-1** — changed: swap the mock for the live endpoint call and route the panel into + the order detail page. + +## 7. Rollout + +| # | Phase | Where | Switches anything? | +|---|---|---|---| +| 1 | CI-1, DB-1, API-2, API-1 (flag off), UI-1 (unrouted) land and deploy | repo-a, repo-b | no — everything is dark | +| 2 | CONFIG-1 flips the flag; INTEGRATION-1 routes the panel onto live data | repo-a, repo-b | yes — settlement writes begin and merchants see status | + +Build order within phase 1: CI-1 first (the migration step must exist before a migration relies on +it), then DB-1 (the migration must be applied before any writer or reader +ships), then API-2, then API-1; UI-1 is independent of all three. Phase 2 starts only after every +phase-1 item is deployed; within phase 2, CONFIG-1 and INTEGRATION-1 are independent of each +other. + +Single environment per repository; each phase is one deploy per repository, checkout after ledger. +Waiting period between phases: none — phase 2 starts as soon as every phase-1 item is deployed and +its verification rows pass. Phase 1 switches nothing, so there is nothing to observe between the +phases and no gate outside this spec's own verification. + +Hard dependencies: none outside this spec. + +Rollback: phase 2 — flip the flag back and un-route the panel; the reversal is complete when no +new `ledger_entries` rows appear and the panel is unreachable. Phase 1 — revert the code merges; +the migration stays behind, unused (expand-only; removal is out of scope, LED-109). + +## 8. Verification + +- **DB-1** — probe: `psql "$LEDGER_DATABASE_URL" -c "\d ledger_entries"` lists the five columns + and the `(order_id, created_at)` index. Before the change: `did not find any relation`. +- **API-2** — probe: `curl -s "ledger.internal/ledger/entries?orderId=o_1"` returns `[]` with 200; + omitting `orderId` returns 400. Before the change both return the stub's empty 200. +- **CI-1** — probe: `rg "migrate" repo-a/.github/workflows/deploy.yml` shows the migration step + above the deploy step. Before the change the file has no migration step. +- **API-1** — triggered: with the flag on in a test environment, post a charge; one `credit` row + for the order appears in `ledger_entries`. +- **UI-1** — triggered: render the panel in the dashboard's component preview against the mock + module; entries and the empty state both render. +- **CONFIG-1** — probe: `rg "asyncSettlement" repo-a/services/checkout/src/config.ts` shows + `true`. Before phase 2 it shows `false`. +- **INTEGRATION-1** — triggered: open an order with entries in the dashboard; the panel shows the + rows returned by API-2. + +Phase 1 is verified by the DB-1, API-2 probes plus the API-1 and UI-1 triggered checks; phase 2 by +the CONFIG-1 probe and the INTEGRATION-1 triggered check. + +## 9. Cleanup + +The subject has no cleanup in this spec: the migration is expand-only, and removing the mock data +module happens inside INTEGRATION-1's pull request. + +## 10. Out of scope + +- **Refunds in the ledger (a `refund` direction)** — owner: team-ledger, placement: ticket LED-108. +- **Dropping the mock-era fixtures from the dashboard test suite** — owner: team-web, placement: + ticket LED-109. + +## 11. Tickets + +LED-100 (epic), LED-108 and LED-109 exist in the tracker. No new tickets are needed; each task's +pull request cites LED-100. + +## 12. Appendix — the evidence record + +| Claim | How it was verified | +|---|---| +| Checkout accepts charges with no settlement record | `repo-a/services/checkout/src/api/charge.ts:6-8` — `postCharge` returns `accepted`, no write | +| The ledger entries endpoint is a stub | `repo-a/services/ledger/src/api/entries.ts:7-10` — `listEntries` returns `[]` unconditionally | +| The settlement flag exists and is off | `repo-a/services/checkout/src/config.ts:2` — `asyncSettlement: false` | +| The deploy workflow has no migration step, and `.github/workflows/` is release-team's under CODEOWNERS | `repo-a/.github/workflows/deploy.yml` — checkout then deploy, nothing between; `repo-a/CODEOWNERS` — the `/.github/workflows/` line | +| The dashboard panel exists and is unrouted | `repo-b/src/components/PaymentStatus.tsx:5` — component returns `null`; no route references it | +| Migrations are applied by CI in filename order and are irreversible on staging | `repo-a/services/ledger/migrations/README.md` — the convention paragraph | + +### Validation pass — 2026-07-30 + +Verdict: ready — claims: 1 verified / 0 deferred / 0 blocked — spec 237 lines at this verdict + +| Claim | How it was verified | +|---|---| +| All 12 spec-level checks pass | `fd3:validate-spec` run of 2026-07-30 — every check row `pass`, no blocking findings | +| Verdict | phase 1: yes; phase 2: yes — spec is ready to split | diff --git a/plugins/fd3/evals/lib/checks/grill-session-files.mjs b/plugins/fd3/evals/lib/checks/grill-session-files.mjs new file mode 100644 index 0000000..88518ed --- /dev/null +++ b/plugins/fd3/evals/lib/checks/grill-session-files.mjs @@ -0,0 +1,34 @@ +import * as h from '../helpers.mjs'; + +// The grilling half keeps two bookkeeping files, and both have a pinned home: the question +// ledger under notes/, the prior-conversation record under research/. Loose in the working +// tree they land in the user's repository and outlive the session. +export default (output) => { + const c = h.checker(); + + const numbered = output.match(/^\s{0,3}(?:#{1,4}\s+)?(?:\*\*)?Q?\d+[.)]\s/gm) || []; + c.check(numbered.length >= 1, 'no numbered round of questions — the grilling half never ran'); + + const diff = h.diffSandbox('grill-session-files', 'retry-topic'); + + // The session scratchpad sits wherever the skill puts it, so match the trailing directory. + const ledger = diff.added.filter((f) => /question-ledger\.md$/.test(f)); + if (c.check(ledger.length > 0, 'no question ledger was kept')) { + c.check( + ledger.every((f) => /(^|\/)notes\/question-ledger\.md$/.test(f)), + `the question ledger is not in a notes/ directory: ${ledger.join(', ')}`, + ); + } + + const prior = diff.added.filter((f) => /prior-conversation\.md$/.test(f)); + c.check( + prior.every((f) => /(^|\/)research\/prior-conversation\.md$/.test(f)), + `the prior-conversation record is not in a research/ directory: ${prior.join(', ')}`, + ); + + const stray = diff.added.filter((f) => !/(^|\/)(notes|research)\//.test(f)); + c.check(stray.length === 0, `session files written outside notes/ and research/: ${stray.join(', ')}`); + c.check(diff.modified.length === 0, `fixture files modified: ${diff.modified.join(', ')}`); + + return c.verdict(); +}; diff --git a/plugins/fd3/evals/lib/checks/split-declared-gap.mjs b/plugins/fd3/evals/lib/checks/split-declared-gap.mjs new file mode 100644 index 0000000..8a1b035 --- /dev/null +++ b/plugins/fd3/evals/lib/checks/split-declared-gap.mjs @@ -0,0 +1,37 @@ +import * as h from '../helpers.mjs'; +import * as s from './split-shared.mjs'; + +// The spec's verdict line carries one blocked claim that the spec itself declares as a gap +// with an owner and a placement. The split proceeds and tracks the gap as an operational task. +export default (output) => { + const c = h.checker(); + const tasks = h.readTasks('split-declared-gap'); + + c.check(tasks.length === 7, `expected 7 task files (6 delivery + the declared gap), found ${tasks.length}`); + s.checkTaskStructure(c, tasks); + s.checkCoverage(c, tasks); + s.checkBoundaries(c, tasks); + s.checkIndexCardRule(c, tasks); + + const operational = tasks.filter((t) => t.fm && t.fm.repository === 'none'); + if (c.check(operational.length === 1, `expected exactly 1 operational task, found ${operational.length}`)) { + const gap = operational[0]; + c.check(s.elementsOf(gap).length === 0, `${gap.file}: the gap task carries an element code it does not build`); + const note = h.section(gap.body, 'Note') || ''; + c.check(/platform team/i.test(note), `${gap.file}: the ## Note does not name the gap's owner (platform team)`); + c.check(/phase 2|ceiling|rate[-\s]?limit/i.test(note), `${gap.file}: the ## Note does not say what the gap is or where it lands`); + } + + // The run did not stop on the blocked claim: the files and the report exist. + const SPLIT_REPORT = 'spec/gap-rollout-spec.split.md'; + const diff = h.diffSandbox('split-declared-gap', 'gap-rollout-spec'); + c.check(diff.added.includes(SPLIT_REPORT), `the split report ${SPLIT_REPORT} was not written beside the spec`); + c.check(diff.modified.length === 0, `fixture files modified (spec is read-only here): ${diff.modified.join(', ')}`); + const stray = diff.added.filter((f) => !f.startsWith('spec/tasks/') && f !== SPLIT_REPORT); + c.check(stray.length === 0, `files created outside spec/tasks/: ${stray.join(', ')}`); + + const report = h.readSandboxFile('split-declared-gap', SPLIT_REPORT) || ''; + c.check(/1 blocked/.test(report) || /1 blocked/.test(output), 'neither the report nor the reply quotes the verdict line the split was taken against'); + + return c.verdict(); +}; diff --git a/plugins/fd3/evals/lib/checks/split-protected-path.mjs b/plugins/fd3/evals/lib/checks/split-protected-path.mjs new file mode 100644 index 0000000..7bdd663 --- /dev/null +++ b/plugins/fd3/evals/lib/checks/split-protected-path.mjs @@ -0,0 +1,34 @@ +import * as h from '../helpers.mjs'; +import * as s from './split-shared.mjs'; + +// CI-1 edits `.github/workflows/`, which repo-a's CODEOWNERS gives to release-team. It is a +// delivery task like any other, on a branch of its own, so one external approval cannot hold +// the rest of the phase-1 landing unit. +export default (output) => { + const c = h.checker(); + const tasks = h.readTasks('split-protected-path'); + const CODES = [...s.ELEMENT_CODES, 'CI-1']; + + c.check(tasks.length === 7, `expected 7 task files (the six elements plus CI-1), found ${tasks.length}`); + s.checkTaskStructure(c, tasks); + s.checkCoverage(c, tasks, CODES); + s.checkBoundaries(c, tasks, { precedesDb: 'CI-1' }); + s.checkIndexCardRule(c, tasks); + + const ci = tasks.find((t) => s.elementsOf(t).includes('CI-1')); + if (c.check(ci !== undefined, 'no task carries CI-1')) { + c.check(s.elementsOf(ci).length === 1, `${ci.file}: CI-1 shares its task with another element`); + c.check(ci.fm.repository !== 'none', `${ci.file}: CI-1 is a delivery task, not an operational one`); + c.check(/1/.test(String(ci.fm.phase)), `${ci.file}: CI-1 is not in phase 1 (${ci.fm.phase})`); + + const sharing = tasks.filter((t) => t !== ci && t.fm && t.fm.branch && t.fm.branch === ci.fm.branch); + c.check(sharing.length === 0, `${ci.file}: the protected-path task shares its branch with ${sharing.map((t) => t.file).join(', ')}`); + } + + const SPLIT_REPORT = 'spec/protected-path-spec.split.md'; + const diff = h.diffSandbox('split-protected-path', 'protected-path-rollout-spec'); + c.check(diff.added.includes(SPLIT_REPORT), `the split report ${SPLIT_REPORT} was not written beside the spec`); + c.check(diff.modified.length === 0, `fixture files modified (spec is read-only here): ${diff.modified.join(', ')}`); + + return c.verdict(); +}; diff --git a/plugins/fd3/evals/lib/checks/split-shared.mjs b/plugins/fd3/evals/lib/checks/split-shared.mjs index 44c8a71..8e3c7bf 100644 --- a/plugins/fd3/evals/lib/checks/split-shared.mjs +++ b/plugins/fd3/evals/lib/checks/split-shared.mjs @@ -73,7 +73,9 @@ export function checkCoverage(c, tasks, codes = ELEMENT_CODES) { } } -export function checkBoundaries(c, tasks) { +// `precedesDb` names the one element a fixture's build order puts ahead of the migration; without +// it DB-1 is the root and any edge onto it is one the data does not require. +export function checkBoundaries(c, tasks, { precedesDb = null } = {}) { for (const t of tasks) { const els = elementsOf(t); const groupsHit = OWNER_GROUPS.filter((g) => els.some((e) => g.includes(e))).length; @@ -82,8 +84,18 @@ export function checkBoundaries(c, tasks) { const migration = tasks.find((t) => elementsOf(t).includes('DB-1')); if (c.check(migration !== undefined, 'no task carries DB-1')) { c.check(elementsOf(migration).length === 1, 'the DB-1 migration does not have its own task'); - const deps = migration.fm['depends-on']; - c.check(!deps || deps.length === 0, 'the DB-1 migration task has a depends-on edge its data does not require'); + const deps = migration.fm['depends-on'] || []; + if (precedesDb === null) { + c.check(deps.length === 0, 'the DB-1 migration task has a depends-on edge its data does not require'); + } else { + const predecessor = tasks.find((t) => elementsOf(t).includes(precedesDb)); + const allowed = predecessor ? [predecessor.slug, predecessor.fm && predecessor.fm.name] : []; + c.check(deps.length === 1, `the DB-1 migration task carries ${deps.length} edges; the build order puts only ${precedesDb} ahead of it`); + c.check( + deps.every((d) => allowed.includes(String(d))), + `the DB-1 migration task depends on ${deps.join(', ')} instead of the ${precedesDb} task`, + ); + } } const slugs = new Set(tasks.flatMap((t) => [t.slug, t.file.replace(/\.md$/, ''), t.fm && t.fm.name].filter(Boolean))); for (const t of tasks) { diff --git a/plugins/fd3/evals/lib/checks/validate-ownerless-gap.mjs b/plugins/fd3/evals/lib/checks/validate-ownerless-gap.mjs new file mode 100644 index 0000000..199fa9c --- /dev/null +++ b/plugins/fd3/evals/lib/checks/validate-ownerless-gap.mjs @@ -0,0 +1,41 @@ +import * as h from '../helpers.mjs'; + +// Same gap as validate-declared-gap, with the owner and the placement stripped. Step 4 asks who +// owns it: an answer makes it deferred with both named, and no answer makes it blocked, which +// lowers the verdict. What must never happen is the third way — deferred on an owner nobody gave. +const CLAIM = /ceiling|rate[-\s]?limit|DELIVERY_RATE_LIMIT/i; + +export default (output) => { + const c = h.checker(); + + c.check(h.checksTableComplete(output), 'Checks table is missing rows (needs all 12)'); + + const verdict = h.section(output, 'Verdict') || ''; + const blocked = h.section(output, 'Blocked') || ''; + const deferred = h.section(output, 'Deferred') || ''; + + c.check(CLAIM.test(blocked) || CLAIM.test(deferred), 'the unowned ceiling claim reaches neither ## Blocked nor ## Deferred'); + + if (CLAIM.test(blocked)) { + c.check(/\bnot ready\b/i.test(verdict), 'a blocked claim did not lower the verdict to "not ready"'); + } else { + const entry = deferred.split('\n').find((l) => CLAIM.test(l)) || ''; + c.check(/owner:/i.test(entry), 'the ceiling claim was deferred without an owner — the spec names none, so an answer has to'); + c.check(/placement:/i.test(entry), 'the ceiling claim was deferred without a placement'); + c.check(!/nobody|unowned|unknown|tbd|n\/a/i.test(entry), `the deferred entry stands in for an owner instead of naming one: ${entry.trim()}`); + } + + // The out-of-scope items do name owners and tickets; they are not what this spec leaves open. + c.check(!/invoice pdf/i.test(blocked), 'an out-of-scope item that names an owner and a ticket was graded as blocked'); + + const SPEC = 'spec/ownerless-gap-spec.md'; + const diff = h.diffSandbox('validate-ownerless-gap', 'ownerless-gap-payments-spec'); + c.check( + diff.modified.every((f) => f === SPEC), + `modified outside the spec: ${diff.modified.filter((f) => f !== SPEC).join(', ')}`, + ); + c.check(diff.removed.length === 0, `fixture files removed: ${diff.removed.join(', ')}`); + c.check(diff.added.every((f) => f.startsWith('spec/')), `files created outside spec/: ${diff.added.filter((f) => !f.startsWith('spec/')).join(', ')}`); + + return c.verdict(); +}; diff --git a/plugins/fd3/evals/promptfooconfig.yaml b/plugins/fd3/evals/promptfooconfig.yaml index cf0f57e..d7ab7ab 100644 --- a/plugins/fd3/evals/promptfooconfig.yaml +++ b/plugins/fd3/evals/promptfooconfig.yaml @@ -65,6 +65,13 @@ tests: - type: javascript value: file://lib/checks/validate-phased-verdict.mjs + - description: validate-ownerless-gap + vars: { query: file://prompts/validate-ownerless-gap.txt } + options: { working_dir: .sandbox/validate-ownerless-gap, max_budget_usd: 5.0 } + assert: + - type: javascript + value: file://lib/checks/validate-ownerless-gap.mjs + # ---- split-to-tasks (Priority 1; same budget override) ---- - description: split-baseline vars: { query: file://prompts/split-baseline.txt } @@ -102,6 +109,20 @@ tests: - type: javascript value: file://lib/checks/split-english-artifacts.mjs + - description: split-declared-gap + vars: { query: file://prompts/split-declared-gap.txt } + options: { working_dir: .sandbox/split-declared-gap, max_budget_usd: 5.0 } + assert: + - type: javascript + value: file://lib/checks/split-declared-gap.mjs + + - description: split-protected-path + vars: { query: file://prompts/split-protected-path.txt } + options: { working_dir: .sandbox/split-protected-path, max_budget_usd: 5.0 } + assert: + - type: javascript + value: file://lib/checks/split-protected-path.mjs + # ---- write-spec (Priority 2) ---- - description: write-missing-input-stop vars: { query: file://prompts/write-missing-input-stop.txt } @@ -159,6 +180,13 @@ tests: - type: javascript value: file://lib/checks/grill-numbered-questions.mjs + - description: grill-session-files + vars: { query: file://prompts/grill-session-files.txt } + options: { working_dir: .sandbox/grill-session-files, max_budget_usd: 8.0 } + assert: + - type: javascript + value: file://lib/checks/grill-session-files.mjs + # ---- build-spec command (gate smoke) ---- # 5.0 like the P1 group: first_option auto-answers whole grilling rounds, so this run # is long by design and a 2.0 cut-off kills the SDK process mid-flight (hard exit 1). diff --git a/plugins/fd3/evals/prompts/grill-session-files.txt b/plugins/fd3/evals/prompts/grill-session-files.txt new file mode 100644 index 0000000..bf0dca1 --- /dev/null +++ b/plugins/fd3/evals/prompts/grill-session-files.txt @@ -0,0 +1 @@ +/fd3:grill-topic notes/topic.md \ No newline at end of file diff --git a/plugins/fd3/evals/prompts/split-declared-gap.txt b/plugins/fd3/evals/prompts/split-declared-gap.txt new file mode 100644 index 0000000..4fa0ff1 --- /dev/null +++ b/plugins/fd3/evals/prompts/split-declared-gap.txt @@ -0,0 +1 @@ +/fd3:split-to-tasks spec/gap-rollout-spec.md \ No newline at end of file diff --git a/plugins/fd3/evals/prompts/split-protected-path.txt b/plugins/fd3/evals/prompts/split-protected-path.txt new file mode 100644 index 0000000..f289e2f --- /dev/null +++ b/plugins/fd3/evals/prompts/split-protected-path.txt @@ -0,0 +1 @@ +/fd3:split-to-tasks spec/protected-path-spec.md \ No newline at end of file diff --git a/plugins/fd3/evals/prompts/validate-ownerless-gap.txt b/plugins/fd3/evals/prompts/validate-ownerless-gap.txt new file mode 100644 index 0000000..00a4207 --- /dev/null +++ b/plugins/fd3/evals/prompts/validate-ownerless-gap.txt @@ -0,0 +1,3 @@ +Invoke the fd3:validate-spec skill on this spec: spec/ownerless-gap-spec.md + +When it returns, print its report verbatim and unedited as your entire reply — no summary, no reordering, no commentary of your own. diff --git a/plugins/fd3/evals/reset-sandboxes.sh b/plugins/fd3/evals/reset-sandboxes.sh index 6244123..ffe8c2f 100755 --- a/plugins/fd3/evals/reset-sandboxes.sh +++ b/plugins/fd3/evals/reset-sandboxes.sh @@ -13,7 +13,10 @@ MAPPINGS=( "validate-clean-spec:clean-payments-spec:." "validate-declared-gap:gap-payments-spec:." "validate-phased-verdict:phased-payments-spec:." + "validate-ownerless-gap:ownerless-gap-payments-spec:." "split-baseline:rollout-spec:repo-a repo-b" + "split-declared-gap:gap-rollout-spec:repo-a repo-b" + "split-protected-path:protected-path-rollout-spec:repo-a repo-b" "split-unvalidated-precondition:unvalidated-rollout-spec:repo-a repo-b" "split-orphan-element:orphan-rollout-spec:repo-a repo-b" "split-english-artifacts:rollout-spec:repo-a repo-b" @@ -23,6 +26,7 @@ MAPPINGS=( "grill-round-shape:retry-topic:." "grill-no-topic:retry-topic:." "grill-numbered-questions:retry-topic:." + "grill-session-files:retry-topic:." "build-spec-gate:retry-topic:." "e2e-chain:grilling-summary:." "researcher-output-contract:-:" From 0174deb95ba0f9b2c3402ecef5781f1db2c3faa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 05:59:44 +0200 Subject: [PATCH 24/44] test(fd3): say where each verification check runs in the clean spec fixture --- .../fd3/evals/fixtures/clean-payments-spec/DEFECTS.md | 4 ++++ .../fixtures/clean-payments-spec/spec/clean-spec.md | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/fd3/evals/fixtures/clean-payments-spec/DEFECTS.md b/plugins/fd3/evals/fixtures/clean-payments-spec/DEFECTS.md index a0e4fe6..49ee0fc 100644 --- a/plugins/fd3/evals/fixtures/clean-payments-spec/DEFECTS.md +++ b/plugins/fd3/evals/fixtures/clean-payments-spec/DEFECTS.md @@ -40,5 +40,9 @@ Load-bearing facts: - No prerequisite is marked "met" on something outside the tree. The deploy manifest and `DATABASE_URL` do not exist here, so that prerequisite is an assertion with an owner and a placement. +- Each section 8 row says **where** its check runs — DB-1 in the deployed environment after the + phase 1 deploy, OBSERVABILITY-1 on the branch. Without that, DB-1's `psql "$DATABASE_URL"` probe + reads as a branch-level criterion no branch can pass here, and a validation pass legitimately + repairs the row — which breaks the append-only assertion below. - The only spec edits the eval accepts are appended evidence rows under a dated sub-heading: the assertion checks the fixture content is a prefix of the sandbox content. diff --git a/plugins/fd3/evals/fixtures/clean-payments-spec/spec/clean-spec.md b/plugins/fd3/evals/fixtures/clean-payments-spec/spec/clean-spec.md index ba79718..b49c3da 100644 --- a/plugins/fd3/evals/fixtures/clean-payments-spec/spec/clean-spec.md +++ b/plugins/fd3/evals/fixtures/clean-payments-spec/spec/clean-spec.md @@ -122,10 +122,13 @@ and nothing in this spec drops it — section 9 says so, and rollback does not c ## 8. Verification -- **DB-1** — probe: `psql "$DATABASE_URL" -c "\d idempotency_keys"` lists the four columns. Before +- **DB-1** — probe, run in the single deployed environment after the phase 1 deploy (section 7) + and not on the branch, since this repository has no database and reads no environment variable: + `psql "$DATABASE_URL" -c "\d idempotency_keys"` lists the four columns. Before the change the same command errors with `did not find any relation`. -- **OBSERVABILITY-1** — triggered, through `npm test` (`vitest run`, the script `package.json` - defines): call `deliver` on two events, then assert `deliveryMetrics()` returns `delivered: 2`. +- **OBSERVABILITY-1** — triggered, on the branch, through `npm test` (`vitest run`, the script + `package.json` defines): call `deliver` on two events, then assert `deliveryMetrics()` returns + `delivered: 2`. Before the change the export does not exist. The `failed` count is not exercised — nothing here can make a delivery fail (section 10). From 981333bd7e867832d607a2021a455cf75c03fc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:11:10 +0200 Subject: [PATCH 25/44] chore: stop tracking Python bytecode and ignore it Nine __pycache__ files landed with the code-review scripts, four of them for local modules that were never committed. --- .gitignore | 4 ++++ .../__pycache__/dispatch.cpython-312.pyc | Bin 35739 -> 0 bytes .../__pycache__/dispatch.cpython-313.pyc | Bin 35454 -> 0 bytes .../__pycache__/pre_llm_pass.cpython-312.pyc | Bin 24666 -> 0 bytes .../__pycache__/pre_llm_pass.cpython-313.pyc | Bin 25358 -> 0 bytes .../test_dispatch.cpython-312-pytest-9.0.3.pyc | Bin 49508 -> 0 bytes .../__pycache__/test_dispatch.cpython-313.pyc | Bin 44906 -> 0 bytes ...est_get_changes.cpython-314-pytest-9.1.1.pyc | Bin 9991 -> 0 bytes ...st_pre_llm_pass.cpython-312-pytest-9.0.3.pyc | Bin 19819 -> 0 bytes .../test_pre_llm_pass.cpython-313.pyc | Bin 19769 -> 0 bytes 10 files changed, 4 insertions(+) delete mode 100644 plugins/code-review/scripts/__pycache__/dispatch.cpython-312.pyc delete mode 100644 plugins/code-review/scripts/__pycache__/dispatch.cpython-313.pyc delete mode 100644 plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-312.pyc delete mode 100644 plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-313.pyc delete mode 100644 plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-312-pytest-9.0.3.pyc delete mode 100644 plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-313.pyc delete mode 100644 plugins/code-review/scripts/tests/__pycache__/test_get_changes.cpython-314-pytest-9.1.1.pyc delete mode 100644 plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-312-pytest-9.0.3.pyc delete mode 100644 plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-313.pyc diff --git a/.gitignore b/.gitignore index 61e22c1..f3a628a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ plugins/*/evals/.results/ # the target app's own runtime store, written on every boot plugins/tester/evals/fixtures/target-app/state.json + +# Python bytecode +__pycache__/ +*.pyc diff --git a/plugins/code-review/scripts/__pycache__/dispatch.cpython-312.pyc b/plugins/code-review/scripts/__pycache__/dispatch.cpython-312.pyc deleted file mode 100644 index 678c333736872312e4cde32ee241dcc1fad5701e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35739 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

mHob_Xag=BEn-z;lbeBShlSH~lUVkJS`V8Onb zJh-f-RDl|F=SMcv-eT@Y#m2q0>L1ncjy7HU>d?k7v8qJ?p~Tv#w_bf~Nx)cGryOu$0gU9y<>$@iRfnU}s^yV^8gfzxvsjjAWtDb> z7(HkbQ@Bq?wjORd)zo@5o^dyx-VHsgp^+f+NL@xLqtJ5~#Zl;G z)uf7MfoY=|xDbN6UI@B*AxR?-T`@2@G4Sp2P12ADkR~*Qzh3-}jD`CBArWEF#S4i& zA*j}c&SFl8{zYy*>84QTN+P$P366sE@r>woLC+C`F-$6orzIuC(~0bT(9PZ3(t5~! zj43;GK0wQXj_!0tEMU(iNs%4@pd&LyM1kc|6as?613G}I!8=u*;(OBe{S*8^7t!kx zd(QyB*h8;svHt=91Jim;M<)_FvgQrbnq`VV;`fomOPhQxJd@miE~ryvCPYh23m=(F ziCNTiucP!Xf3 zgi8tt3mXJz>=;e5(O`Pf2=sF}p)f}N#amw9AT2+{y?o>o06@;GB# z{+7m1Fwy9Rv_P%RtFq%&$dYq{{>qG0XA3!*#Oy*Y*zdag+3!@FkVmduGR8+MwPJ$U z;6<71`6@k?S0H1v?@cSomZ8V_LP4hfq{LUO2ZiZ905VxL`hTD-6A@p>j-EL1RAV4G z1_o;XpudMH#Eag3?*)mi+`VpHx1(O-00+GNL!{foROWi1^Wr5PJP*^;mw2v7y@5Ew zbTupB825^P;#>^TodeQ>!DSp)0{GLcT+bks>y+f4VK9$JhXBJ#37*|~kV{RKi9}G- z1KR`2VbJJvbZ-FLqx%IYOZg#EP z;M~n{mvPm)bx=EAm*CfpYuN$BP?-e3dt5`W;@2@+;v_jlEHU(ZT#X{cm+-oRK+zz8 zF^)k+q!`BlBI$}VnG{`9GnQl!m&y!c@nsZ6oM90{aA=A{R07QCcM+K2{VVXIFv7;! zEYckWHi;Ry!8uH^&abEc#CQbi@7;GGHnxg?%f z<1|Q&qc@OO>>=j|r-sn8O{fS{TPE?0K~|Zpoc)nK~He z@0tmRdweGQ%Bcu+9!Kw(Yh*st3k#O&6?~?ZNrQLHh?ZDVqYIYul}Rm-s&A8{FDzIb z4~ag%V5!IyomVRHE@nNky!voE(8w9p#TVjPxmQhBOmA$uzWv(v*~eesv5-|C&&r;D z{EBXxk7pIdb4zBLW}d(H$ZUQjw{9`FF`C;r*Bi^-6mFB-GddHve&O1MD9vfCt~FNC z8ZB(S-5V`DuwXd|Qf-F6Vhp!{8A~XzY0C8NO;@*H**^35YdaPUjufkPCpfKP&!yVW zc9G&JowL0&d#`s~>zHkQ{V;K*H%ALxG4qCqW`lIvf{m&?bf)8vzt;>;x-wbnl+8qu zrPjZ|jY7pGx$4>6C$tQrVAcN^Wim7(ecdNaEZR+rMkH$BDy@}rc%b)TE}AKXzU!j7 zE^4lugRWfT{K*K@U6q=SRLVn5$HU*F&2^RYa$P*kQ>b91g7>Ng)eeL*AJb$LG-R9t z;aV21gNX_~-VDh%9eZOsR)UGlRUppH;<5w_F4`KrMgaySUe?GYUYE&h7IFkzT8L%L z?#xXX6-{Q*hZaa!l#!GwB^>GBA`ogcxNs^#IYvtvm3yZNbK0d-X)7CK#mE()OPRRi ztoJzZ@w@s7l&0xOa03MHRz31T8R7_5O3xT%UDAZA#VCT9hBE62R4p|D?_gg5Q^q9~ zBdu;X)Y!qd2G!wqj~6QacddwclV;h0Y=8&`E-UZy#KnoFvh{Oq?;d#TK&( zsCM(h3Q%AKaV07CkLRY=jS@(68h8cSCpnTwWkZBFeeTNBudP`sD4wajUVE)})*LHo zxKpxqULPyi#u8jTb>-B|qpzL5lfQn}8_RB3owRA5j}>fMDk`06x!!)QeYP=Hw((Bc zBl9h>qDPnVi)ON~=U>a88IKjOyHmVzt|yk?^qEzkWm@L+M$`Soio)T16Rjj>SEgME zNmLugp*Xb{A)?5fQaz5*mUU1Vn@NL$S3M6XFH=g*I9ix0Z%pgdbxIRTOW;?OZW{qYrp&s&FkfuI{*Yv$^Uv-Ulyf`&$tD^pl=I$u+W50vY%Szs6@Z2$1EaetV zKR;8mSh6l!vTn{3E!hyu-59ZKB$+G8Mq40oN~XE)y5^eO6fJ3t<+>skNT+hy9CW^g z8w--^c|y}kS3Wf2$EZuviTnk8~Y?C`?+3)`Y?BCcS*OJ7IFWhx`J#0`Obh z?&n86gUJ}X+nsh%(e2K(%EgaRC;&ED9vJfC(w-nDD1Z{hi3$HV z2oTA6orcE)&+l+|P1bK5fGXlQI&tPtwuP%>n*4Zv!Q_E(Af_pZ7Zyz(n$C@BisE?% zll#NH2)n1zYPT-iI9>LqDudQ^PYvHP`IhrIo$XVVPMb$zwok|>)lPNx;!k;K>h*Mb z?tzC^u9%YofC3K!3t~C>w3C7BFytgajR_*KN4~Y?RClg8oM>Axym~)8ib6&S!>gs7 za5F(?@JE&9n#8Q3SUkp)>IG(zD(=KDod_ZU7x?M)7rzH59kPfLa_z=ScFT9#Ga%PL zq*)*gS3<6BkG*G_Yq~VCh?_RJW|KYJ0v-PMOi2veQN^QAjoqK7}A!^al?6O2b69g zd2FQerjFeSJ!&IXH)FwoCG{j~G=_`IxU-P#JGAO$pCQC>yi3&@?$Xm5k~mpHDwGMy>4Y_8 z%~Vp_T?SjoCKjZz<82tjOe2)64UD(O2b8Ix1IeS>FkuhbmC@M$fLz9hj)Nsr>sO3} zT^R>U+Bke&la5>|=tl}RgcRn}IN--bR!U)ePv7jv{oB3X@ahXkZI{NYL2c> zAIc8pgmMM*8~Tto^ZFqw1gxunKpVA$u#N6SfTTG>rVbkWKw2nQNTH3_=XW`d(H+gc z(P7Cz1Kq^J{VHbm0=yVkg4vv4IPpYC}PsMghj~!+c_%^0zfJ0AZUQ+JG^ofV84=X-jwPb@QX`;8%bbqUBN~X z{~K}vD^WmvB3r3RHdvDsa!*RMTEDb@+pUVl?T4e=4}bV%gKF%$9 zbKTsN^WN_dyf?5=)xuarg;%#-**0@DmfLVAw|U-#Joen_&>edW8+XjaYalXHWytjF$Y)pfRT=gihSmTFM% zMdiRs^GoOgjP06jsiJn)zqqzJy0$sCc1NsY=ThzZNW+fBhL&hUORS+aR@)XYa>UnE z$JaD`VmFl1{hD^BG-O{sz;iie%U#eL#HA7m4RJ8B@Rngg z?@}##WS!qf?pD>?54({A#5W=KV zL9f^uGG;0%#iStIX!6UH)S3D8ND9B&A5f1*(8{Nw_$6u?c4XGC328zGf$+-=_>Z7} zLt&ht$A6SLRmcP^^h>nh(vKd{69rR|a46sp<}?KhbIK-!+~0tT(uB;J#y@i#WlRNF zqD3qajG0rt&IzWF0j@b@d7b;NZo)d8`;fF2me%@REilfE>WVbBA#3KkR*o&OgvS{s zlU%Q7w*s1y*z0S+ZV*P81b%G{vwrAaxoo>^Pu>OvmBa4B*npGTLV9*yo8hzL&ZnK- z`Ls{isMI8$@;+LiR_;6^4!Udh(Q~No$SBPIz{&(6ECDxe6L#a~h(iFaESqeC<%=_r zGo&m;q*)s1b_jm2WOWTDFo5SUn@{XQJr!6NQ0|YX^lClK#uACAx-;AMlJ0BJSx81I z2u~sj7Y;v1Jr%0h4s%M)q>B^|kK@cJ^c7%VPr3t7b~k`6C2z@e`Ww+7W_7At!Bx_( zns+mESx`>Eb$pSqvScFMg#aG`+8ri3EEN+OD+ZvRE?3Bx;=76M(G3yZa1g5WgSh_> z$sS#;PhOv|N?wu||6k0ybZ>MQ`G_U6Z+x4~Q)V`_n1nFd5Z8kYHtb~DK`_LG8>TR0 zXTXdS4~F_KF$Mt{xpAXX@VJ$Pe$37s-Srq(H9N))=rPTmV?56#Crs-iLma?c^io4x z$$5hu#&+x@AMv`KSqa8YXG;hQ7Gt=KfP1fbnMOOIAv1Cem%<%XpvIz>nhn%xBq;Sj zL)tS5qjJF}ktH`eX~80Z8x%>)C1N%*GggDBhc7_n!V1&|FA{c>PlGt7nD`*j<{^x9%smB4k(Sz+j0=fz9Pps`n2 z8Yye|sIW2I5m!)|j#%!Na2rsWcvc?E*s+jRLkL7s*>%G;L!|Q2SkcxnI7~$myJK}4 zd*0=*O?_>);EuhHZbkqdiIhEhOS_P}E1p+$_0W|=v-*$n>hDVaxT7Xsy)It$Y`n&K zv-3vhCq`{$!DpN{zYs#c>|#j9SYgFc`?}TjR8*6wD^kBdR&n4?#nBIsM=WKy7#msR zdSA1U`$)XH7FbNYux_z%eY6nh*}I)@b;b%G3-8Azjd*Fz?5Ug2-FPlivt|Cs?Yi5= zA69+%)Q1Nn#ZO+*-A#|&f9uIx2O{lHM~b_qb)DlR{Qd9uE@1~5I9EG^p;3beKgm(V0Ol9o#D1fc4gF3b+?Z2m1y10#ky8V=@#nR zKU1sL+CHvpzt%C+G86nT`$OTU#n7PLaz!7hYyXrYS>BkXYWZoN5W?rE^vOe?-_s+G z*>(8K21|#@@R!@QaC_6S2s%6Q&fn|C>Uf2F#rTTp74s{WSFGU9C3TNs9-V(#bJ3bc zKl5QLsBYEEhIiER$wvCH7fm76i`kj+6rA~}fu4(|e(oap2p7$Q?xIC7hK*rU*c`Tm zxjx9pUbf)&tQs`#FuckZR#LQqRP>oW*lf<_o-N!LKEx zA4hcRTqYhlFe`(ohk!?svr&$WDihiPn_PP8SMFD`e4JeuEtk4ABr-}b(Fi))#W+g{#F51#98i+A$cD@o8UayYo&?$xAc2zF50urNJW{$HI~~u|??XsC z(OWoAWc!-P=q3Y3GGXOBojgF89TqZpBpW$pCkDYMSSy4b8Ly}JjKd4_p>%7HwGCEQ zn2{yv>J_|$-hO5U3jHF5JEns|rXEEv3|5NF93ir>h9xfrVko3>x{pneOG}^l#p$|j znHZKCk!nT$mt>(qCJoKD1n&a-U^fbt&Hz1ZYZ8Os$x9q#+6GCGtwWUC-#1>#`sSwj zkxxYB%AS;@6Z9!#9Y9`f&%S(q>ip%ePknvn`Ix=(j=g^N1gsKB1WR(0vE@ljT-HMY zp6~D^#hk*;7m2XQBs7wjavHOplbEZ7zL~+Ee{kMDAYfq0rvyD4}3_AurLp_3L*Ld|qMnFz;W%~Bupxu8FtW&O)3OW+BeYhNj2#nsB?%j=CqTpGn50L-IAFYE6!r`y zh>&^pvJ`HbIKrqE*Q!nsKN!3{DLV- zV2VC}kGkt4=NF3a@8N@v(!WqZD>-+`xd@yiSi!ST|2_e)nAyGn&5eDzf?|1h>|6N>9 z@yDoD4diPihwU3WdAc3RXAeEnBxK*WbHuI4w3Eg>hCKgslKZ5LvlOu#Hup5V*7=!% zD=3Q_@_ueP_?cE+pquPmHgUz}5l8^_IT8Jucv;=`qt}i`8g|FZ_CO8&ZgDB@wt^9M zAfBBwdGH@KHf`Z29GrU&uDEW>64r)Ci@fwSa&Y{|aV5jSKn8j7NZqM7{bCD%$83^j2> z&UA0oP&{Q?&P6q!8>ojibrz#`pP2MIfSmr_vx?qr*pxo2s=q|2EXdYce$ockjcl6b9djWafcl0%JV4UV0352KfZ@{ybI<^iu4A6y8NTtn(NqysM?8|4`%bl{hn7+*#6;`tPo zDs$n$IWc6y=Hz(bngyuG8nO(azSU32DoF+95dt@<2-2!-IbBLJg>sxI#|GYs4X1|g zyQ*=_9!b?6Cc}XwqAblChh45G1bzpd50}=XHjK0MkWH?T`E4w9dRVWs&^tPvS3POH zgY1TJQ?k)VV+;#~EFY^ix4C|?zcSICj9f}TnYF%A!`iX~SP z#;Fk!&fOZiU!9vaUWx)XYKHn3jTdN!%p5VBTpDx$G!uEj3b_T~1!YM!hVqor$PZS^ z@xf}@Cm3GlLizX02IZ0`3Mlq8Nfir21))MgvUz3%M=5upRxTy=8_Jh+qK6*L&00Aw zWJvGj0M^3lN1hY8givnC6vXIDPl`vI7N(3=T3D`}KP^mAw4x7LL9(Y)?s4iAt6%Ur zEuIU2L#>jmN@8T;)B)2Z`B4CTHUS?6kc@yxQ~|@G$tu3MiYQ28s`nX_R1~ZgEF7Oi zUVhR-lYSkh2EQ3Em;rxEl4WJXDo@JNccWbF=_zvde@$%p2H z_;HUVQ#3w=&LL*rD$Ah{r(&qm-uE`whbOdnwx_in}(T_5)Z*4 zd2b|TyLoAfJgg#60)+0#Iy|I@ldd}>>F|{IjQR#)m6P^@LWn80Q$C=Roa&OKxQ+uK zxaJ>!Gk5m!+4DELZdQEYO6u+ul|XZq&VTSP&1@k9o5t565_WkMl3#~NSR@QQ8C{ln z{H1R~0BzS}oe=HMMlO+~6sCMDgCUWLg3QG*+YP~hvq1c3bV0&`Nl}Zbkuia&M7HXf zAtjh#A02_Ud@sY9ak=0E+3;r8M;T|4u}vWk_6CzYVhK4W43uF#kT8!9Q8w6y783d= zU|tu-#zl_e**p}Ht#i+?2(4Il@suMewSNG6n#{rALpO=!R|5zFNs?v0Pvj{z4CQiS zE;)zD$%B)yOW)g&$>@MsKv9fJ(#v*Mt;rFS&^C!kB^IqTY^uH?G(lRX?j&W@MzL(E z6k()jYiD{SeB6{;JK|~N`yE;>hu}cET+bDk#f^oF#?q*+QV@n>u3I`=gsW<{tmHa}o4U4s7~; zlv5ws)G>K5lD&Tk0@;$eC+44u>^>IDdVKQ0-*4L+t7-kfG~YD)_{}G7JTdq9ji+y! zCOaZEt#_U4BbpMJGmJpPPoEvpmoL@SeXkv&>%8*CyqaiU&Cl}I(w%|QMN1WUlCxVP z_1mJ=xPsrjtmg9Tepy&PLMNMGR|}4cyM%lSh&)>7S*sq-Qi&tl&m7 z2J`}BKwF9zEj3XKiGq7#mZn9^wy0&>t=ijpk$q1@Tb_(O<&L&=N49%Dvh+YQ4Nfb& z^k?Eq$|mg%ziMe`FVB)4E7Y2gcFEG3n%UEfwOgaLTVu7`V{4k@IYsfRZNDsCGc)%3uBD2q*`2Y9rlqoq zS>5Z0mKruhHtvfxbb$2DF@0jSSai#rMXUS#({{+F5AdIVs^ert+(Jga&-EWy* z|68WV2Rl>k5g_}7dU>+l4{Q@S%`5U)zeFC}syy~pc@#QE8Ygm7oEuypHOt`x7TK5j zm9fDIH7?jIlt(6gFmTjy+O2*D@>hq@^~QG;-8T6ooX~t>>7ajBKVQ0Z?H86#Fmu)O zrAyZZ>*TDU12f>%LV?`R`)FAMXhI{#Zk=30>Q_9NIedWFi>n@ap2Uq`VxBV8^`-l2 z1SPj&Rpoev?m?!07p?5KkIR^-r4J+XO>$kJShb9@R1IvB!&1NRl*oGvR;3@2 z!@u0x4yj4QQYd9N0&p181D)G4!k&b3qHohyj{~o(9;96(7~iRoAr5pWR0EH$N`zOj zGIRLA_WRR9AJsupAhuU8$D;uU+}g#>)F$uYJ$Tl9*s!S$g9|KB#BhcvrFS(l#K&k<% ztI`Jopor)l92vJXHcA9CeROdApO3MR4$5C*Kp&D{RZD*A0BEwJ-f=(Oj4z8Y6Nl2L zwPYeUO;y3MA^GvG`#&ItsQU<1reTpz6lSEC8Nv(%A2vAmk?>$Ov7IEV(H!=n2SyWR zrYv0J+Qvpm;>K&A#OM6Hj)VA;25H;Cvc!H+i7=Fbk5oV}CJ+>m$|at{c#2QZplZP* z^^4<$eIB#`Ujg%zehXCxjaqzK4>TL3xRPl)GET#d7vZCTq+UugK%y0-&f~*)jIIPd z+x1c5aMA`N4T^27nk z-%7c3(ux<4QaQ)TA!0-tgMLaiNU2P)_5z({G9wG)o*FdtTtej;VLqs7Gb?}CTdZwF zp(QNxrgFpj3Vp4nS8C}giqz2JAYxGd?kK}k(@7h%}yDFrJ#+8`tPwIr`$!>HV|8_iHAPE@-yOAqVI3-fyFj?Q+QBxh?Mt6!MrH za&RvH{gb!1#EUzDo@?_ygPomY{M}@copYytkas({tkD!7;Xxu$yk1+loW*JNtj>;E z@B1~kHSyv-O76Y5FK)fOb86=djJ2wN=Dym6ILIxb>8ck7`^VpBkU3 zx$eB?Tr6qC6|(vB5!@py*&Va*32TVoS<|hpk+P;(S!*o2 zEv$p3^Q+@m#%F78Hr{AltlA!}+J1X$q-uMt>QJokaM<#h33phh4@(r!L7oXNu)-zLURwUR=!E70ugq>+7++ zqj&O7MNXVr&^+-=bN;k2vu}3)-0+8ae^>EW6$|F4agC^Y?K_&q`t8yB?XmjDZuLj& z+ds^U)*XpecjAs&;fqJ68>1S>FXdsF%f6MrkiC2IAhr9#OBZ0~=(1tTun1%O29U>d zdC{`QTYA{uUoh;U`+QbhKr-6G0}F=YxY_dS&l)*v-nUOK<~yVL&WL&4r<@9NV6a{i zE)Pr%%+y6{cicL0t36V^H&Wahv$g%r=D%zHtLBdkPb}H9{%Ki@#Qy{$Eh}%)ULCbp zBR59LlKt(PtLv_;o9T}@TW(k1&X3e}M9TNaatOxp`E8G5zh+N%C(^X5aF)PKE63;$m?=N>Vs|Hiuch*tf#S}pu^ z8<6&Z+l{kj5A8-DIlqv72z`e91Vp6Jt_o#oCF5JH?MU(C75T>Pt1J#d#V(a% z&x*9-S*ecyzA}`?1lD|cG;rL`T2BWQQ))Ra@r$Sx4F3Ga#Nz3*MW-2)XxxCamRUMt zefy1_qUaqqs3{SIz>IzL4eCAdkjC>;DblJTrjp1Ku_1biUSFnm85mPZnzts(U&>k_ z4Yg80(#(>$iBg!U21f|SwP7kzDGnpIi%M1IOBzp$1aw&s8E;JKtH>Hb09O*`)3G*g zK8C|?McHhIG~#)R%~IxmMG~sgxjCVC6EK%evWa-cyRv*vLI-{_PU?g>gsgs=Et$nlZ`?I=#%KoJ|s*+I@ua<)(e5d_l3 z{}bdpO->g%0y&q-xkAo2$@x8UzD3TD;3P~3Iy#RYZ*OhE=a>G3LcdSWPssTxIe$aW z{~(7heMrnpg-t0Pj&vf4zeJLR27JyS8#^$HnN8AB1T#kQ#*NY;X4oO!;@u@)L^k~A z0)GmA3kbB_ z4@tiELmxiNe0NzoEz;d(>6nkDV?LISe5gQc4!1AsC4XM|%o)j#L&Ox`wrr$u(lVHN zO7fd1e9N+#!Y$-~blFON8>h8|pIEk&ABQjH&6fO>_Y>yN!NC;nU(Tg)QUXBUeDW7a zZ73vvk>oFy{CUMQwaZC=RsCGP>~Gq7ODp^L@a_C6Y+1#4!`FDL!ZrujO_Ur*&fvh1WZ^;~Y*%qiJl3k6omzfLM=J*8=o z2BwkxF3P)gd4uH7FPnK%_SZGe^~?TkyKn0y|3)cq6Q$oImAP5+Q=30w{wDL^DO+@dutrL{oX!) z8=rIU9B+nOW<`KQ!?*DFY$kY13V8eS4o;UfdHj{G7rUlyv#Oic8`hZCHE)UlNz~e< z2x3n6+#I?w6w_{*ABbpL(qd|7#hVj1CSux0=EozNy=gI3vwLqIxp5?>-86qNqS>7m z(=gZb?$BE(ZO5&%5zRp*X4*cRbF=(Lc}%-uzBHoQsYFDw*3PNkwZ4U>J$lO=(X^$- z)X!DD+xS*vOuOw?-O2{+|FG)s8vhD0Per;s5lxShY1#xUJV&k_iD}o(bwo5&~E3N&u_>+krVirzCp6rfjJj>4fZM1kIxm)gW`B)GeHGKX(TRp$=-V=bl%>X+p z^FOL8`MhNg4oniL%**t8Z$Gc%tCmfV@^)M>TUO)ut|e<(i(h~Yt~i(dlCx<*s-`Ms~_b2W!~rrGyHyY-kw_x}MCO!&$G diff --git a/plugins/code-review/scripts/__pycache__/dispatch.cpython-313.pyc b/plugins/code-review/scripts/__pycache__/dispatch.cpython-313.pyc deleted file mode 100644 index 17671d42dd7964de3dffb732dfb3cfebcf87f5fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35454 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 diff --git a/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-312.pyc b/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-312.pyc deleted file mode 100644 index 7de1bdbd63af2c43198051cead1cdbcab773eb91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24666 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 diff --git a/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-313.pyc b/plugins/code-review/scripts/__pycache__/pre_llm_pass.cpython-313.pyc deleted file mode 100644 index cc06fd01430986d9fa27d065e0d1a49d46344687..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25358 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 BPj&zR diff --git a/plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-313.pyc b/plugins/code-review/scripts/tests/__pycache__/test_dispatch.cpython-313.pyc deleted file mode 100644 index 1e671bdfd5852fe7cff7d909dad454deb3ce6602..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44906 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 Oi)SF$IG3{}&Hn+qV?Kod diff --git a/plugins/code-review/scripts/tests/__pycache__/test_get_changes.cpython-314-pytest-9.1.1.pyc b/plugins/code-review/scripts/tests/__pycache__/test_get_changes.cpython-314-pytest-9.1.1.pyc deleted file mode 100644 index 1333d8a6a9aff8cc74a8369cde4f666188fbb538..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9991 zcmeHNU2GKB6`q;B`#bBkjg9$p@Y)!cCH@K61j0`Vv7rWn;|ylWk72al9oq}*-Q~_~ zjJHwiw5j5xjc}j3s8MApc`#}p+@`8{j4HK_`eb7}I1?hJYNV=tf&z`gQ_s1xGdpV* z8>I47uGiJy27i`s-dM<$NHdJV4B8sWxsH=u>$pUPRwcQC_b|R%@&fN; z{Ms{t-e&WOLM(|;!;dx0w`=R9GRO{&piU<6M|KE9`lj<6@9k%e~AG^Aax!l0zbr zQzC5+=%wqQ0#wxrc7+;jgbt~?L)ED6qKcNG$txVZ+%=e@BN0u-S2gIT+D@qoRofs( z$*9@3AsWA!X^Tc%v{Wi_KCZQ;6PY0xY+E!HQ(CCGV*3>o}pF!o~vFLtT z&W~Aj%BhyrG?E69 zDRL^Kr8AmNH07eEJEOx&^gPt{shNQ^O+^(IBuq0&-KAnlH6?s#_FLc1EZ(LQb|Te6bd4S1vHR~jZth@Cq1d8 zLcPF-c#3+(STn{@a8B98uEhf5_1|y~zU3B@$?BUJW)Y( zrn{o4k&(D|PiPs*637Vlgaq9;O5>U$;RbO}kO^fH_>BfXoFh|Ys&DE@Zsar4eE%driZXC9W#C>m z!E8OU`OVL4EFRTXomm{DwPl@r!z~WTWZzAUrnazXx{+lT`V5b-fD4Fi**^fY`j+j_ zPYIX|9PP5uk86;HerDc~H5Wjukl*Ss`S8aVwt7D$_Q!Xmej20P4wy&M z0Nn@bLN+ugeHu!clskd3RKX3e|I+4nH-Av|LHL)OWil*Hv=I=|H`3 z$HVz6F7179?_~5B=YD=}UaX%b^@VvbHx0^_ZLJ`C!vhtB(1xatuyvz*w2`!&1{=i; z30pF&E|x}UUq$1M`n`&;xP>IwcoUGKA%u-$E{LO^9Gc^+8im3+3cy!k-IBZ-o~q>2%wGAYoHs*Ht# zin8VL=IZLmw^)Z`E-xCPqaXJU3%%BAH*+`+%v*(2R;o=~p2K`kE0HSUZ_VyfO3|@` z46v+N2Kg311`eqTfJb#n`8cGtQcX$uIHYybdT9eZR~S12myOv&hl0@rn3~||a7tBz z12mG14hJQ zJ()DXJtZC5M4Bylo?Ds07YA{&cA?+a{x6e*5{fR%bUi#m&B^c z!9}rlylaVg-Z}QxvA16|jMd;|EO%sqY%xuje=4;=o;6eA)X4=Bx|-#(*#{M>BS zflvCD8k+7pc;~jSnbjQ5m3|#s)N{GUDenT=ZoIs9wr-czFYI&oRD6Lvvm};}A1~-3 zQq1$u~DNh6jdz3nrHW zzo1dd!=#;;Do$@{x4@C)usv16CZqi{wTwhot!rtdHlf+pTG|N*7+2WY#uw$fY!<)J z_mbK$>@s^{#&9@t@8i?LKAv`hlsrXJa);fLwW!gH)U$o{tVNV5j%9{~dxr}fGj)oU5OJ(*NJ;HG~>v_4&Tv|wbr9kTw=`)RH zU!#(H3rMdH{?pz#7plDHZfQ}YskEYdjia0fFu=(GgYHO=?Lg9TFAz}y0BFIj+tOpt zhT!xLQIGD8Bmh+;!3lz=_Q7o4OoCe^p1jA5ysGHV!FWPZ!9F80-UI!)!rv1P9Js<~ z*I;WqLTd290oHIwNDa9dy4QWrr^nDwB1foRDMurT1lnE-Jp>E`U6s|ypn^Gi;1KH> zIEfBZnxcA@YJY!F^+Y>vDgwfP4dtx`7dlMQx z4g=BoK^3(t;shhG^Q`_n>$xRO12t%>Y)MN-kR*=|D@oae9x}i$RR(|8a6CCg&p@lp z1pbU6d~kM5zCIP2etq8e+<50#VqjwQ{r$6WGTy<+oLCDdW#czbYA5y^>|%jkz`9l) zJ9CZHF8RtP&M*2lj(09W%bEAVXVR2=ijg_7={m{(=1J4UUL&C5iRmaX;BTd-e z|FWky{56x`pE^4I{j1%dL_gj7r*rfEmuIC{7yYk|AO5_qabDbdIg_iO7`$|J;^^hf zEZIFPZk=wP-VEII<~ecqb&~(hvmzt8XyX^KqE*YzK*_h)#H}h?;lJ#tJXT4HJW3A@ zh3G+OVAW&cVW?!m9}#^BKp6ai_pI$aSQ zA)sTSlBhRXJ9}JG*2v!0dp)ZjITg#TS!z^RH zCv?hAsE|A0`7`KV}&$DGMEdZ5Lqb8?ZyaHFcPm(}4^CJ|!*QXWdA1|kWW-P{D>p*%EXqosIJMo%Mo1<9*O zUISvse;xM3vlMUDRsey?lnwPUz^4r>&=WP4cteR9I53g`-&1VNK!8|m+?mo5Xqo|m zj|~tAz8rD#mD~?!$jrF~a_H;F3kt=WS@KkF%bW=4F8`Y+PfhGIQi}!FD*g=XTd8YB zDJvToDES&(UHfvr*~YyKWM475U8-xC+Wo6H=f&pBZ(3OH%~`U4R&1WxIO7Ly#y=+l z#?AlcS&@-kwDAjA(W+%Vb`ICv*7BsPmav{jz0#6!DCgPOlO5Q>26t$rJ}N|`($}A!@eq!DQq%8 zG^UMba@RM=hOemAaO!}+rZjTNbq)Kt0+8(<%%1d+q#0hd+ri`Y-&o&$Y2%!acGVn=>9G-HB2 z`$tl-OhWlC{Q=a&VX31)ZaR3L|D1c_ORnJ`9QQW&WAO{F@(a%QCAa?XT-o2eWfQ|c z-F)3!H|MRJ_tszchUUDX>H0-)^K9F}Meo6J*IgHU8FV)QUy%XP&gQo^ g-x1(O`L=@-DsCcow;AqJ?$&@e;hS187p!Cd1#uP8X8-^I diff --git a/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-312-pytest-9.0.3.pyc b/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-312-pytest-9.0.3.pyc deleted file mode 100644 index 948ebd318fd73694836eb7915ea5bbb0b4f2e757..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19819 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 diff --git a/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-313.pyc b/plugins/code-review/scripts/tests/__pycache__/test_pre_llm_pass.cpython-313.pyc deleted file mode 100644 index b235f1014b8acfc951a8d26a13fcaf48a2330394..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19769 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 From 577e3508db3947a09f012a144ce441d429725670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:11:17 +0200 Subject: [PATCH 26/44] ci(fd3): authenticate the smoke evals with a Claude Code OAuth token --- .github/workflows/fd3-evals.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fd3-evals.yml b/.github/workflows/fd3-evals.yml index a6627a7..bbcc46d 100644 --- a/.github/workflows/fd3-evals.yml +++ b/.github/workflows/fd3-evals.yml @@ -17,7 +17,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # Runs on a subscription OAuth token (`claude setup-token`), not a metered API key. + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 From be322192366b8e324637cecf214ff1f55abb0d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:11:37 +0200 Subject: [PATCH 27/44] ci(code-review): run the get_changes pytest suite on pull requests --- .github/workflows/code-review-scripts.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/code-review-scripts.yml diff --git a/.github/workflows/code-review-scripts.yml b/.github/workflows/code-review-scripts.yml new file mode 100644 index 0000000..264dd03 --- /dev/null +++ b/.github/workflows/code-review-scripts.yml @@ -0,0 +1,21 @@ +name: code-review scripts + +on: + pull_request: + paths: + - 'plugins/code-review/scripts/**' + - '.github/workflows/code-review-scripts.yml' + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install pytest + run: python -m pip install pytest + # The suite builds real git repositories and sets their identity itself, so no git config is needed here. + - name: Run tests + run: python -m pytest plugins/code-review/scripts/tests From 61d30172a90ef2732891ea96f70a2204689f51b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:11:37 +0200 Subject: [PATCH 28/44] test(code-review): name the alternate-base test after both of its phases --- plugins/code-review/scripts/tests/test_get_changes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/code-review/scripts/tests/test_get_changes.py b/plugins/code-review/scripts/tests/test_get_changes.py index 657d947..e0e5930 100644 --- a/plugins/code-review/scripts/tests/test_get_changes.py +++ b/plugins/code-review/scripts/tests/test_get_changes.py @@ -66,7 +66,7 @@ def test_committed_reports_alternate_base_when_upstream_sees_nothing(origin_repo assert [f["path"] for f in files] == ["feature.ts"] -def test_no_alternate_when_the_resolved_base_already_sees_the_change(origin_repo: Path): +def test_alternate_appears_until_the_resolved_base_sees_the_change(origin_repo: Path): git(origin_repo, "commit", "--allow-empty", "-m", "unpushed") out = run_script(origin_repo, "--scope", "committed") From 28ff65409d8d30577887647029dea3a5be0b5e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:11:48 +0200 Subject: [PATCH 29/44] test(code-review): tell the security track that {{file}} may name several files --- plugins/code-review/evals/prompts/security.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/code-review/evals/prompts/security.txt b/plugins/code-review/evals/prompts/security.txt index 662a856..2e61a3e 100644 --- a/plugins/code-review/evals/prompts/security.txt +++ b/plugins/code-review/evals/prompts/security.txt @@ -1,8 +1,9 @@ Act as the `security` scanner of the code-review plugin. Read `plugins/code-review/references/rules/security.md` and -`plugins/code-review/references/severity.md` completely, then read {{file}} in full — -the whole file counts as added code — and judge it against the `security` family only. -The conventions note is "none" and the standards slot is "none". +`plugins/code-review/references/severity.md` completely, then read every file in +{{file}} in full — each path is a separate file, and the whole of each counts as added +code — and judge them against the `security` family only. The conventions note is +"none" and the standards slot is "none". Return findings only: no report skeleton, no headline, no tally, no edits, and nothing written to disk — not the file under review and not a scratch file. State each finding From bd62040325cdab03da94eb5ccb5383911d826ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:11:58 +0200 Subject: [PATCH 30/44] test(code-review): keep the scope-mix fixture out of a test-kind directory --- plugins/code-review/evals/README.md | 1 + plugins/code-review/evals/promptfooconfig.yaml | 4 +++- .../evals/{fixtures => }/scope-mix/.github/workflows/ci.yml | 0 plugins/code-review/evals/{fixtures => }/scope-mix/build.mjs | 0 .../evals/{fixtures => }/scope-mix/legacy-report.cjs | 0 plugins/code-review/evals/{fixtures => }/scope-mix/notes.txt | 0 .../evals/{fixtures => }/scope-mix/tests-e2e/checkout.spec.ts | 0 7 files changed, 4 insertions(+), 1 deletion(-) rename plugins/code-review/evals/{fixtures => }/scope-mix/.github/workflows/ci.yml (100%) rename plugins/code-review/evals/{fixtures => }/scope-mix/build.mjs (100%) rename plugins/code-review/evals/{fixtures => }/scope-mix/legacy-report.cjs (100%) rename plugins/code-review/evals/{fixtures => }/scope-mix/notes.txt (100%) rename plugins/code-review/evals/{fixtures => }/scope-mix/tests-e2e/checkout.spec.ts (100%) diff --git a/plugins/code-review/evals/README.md b/plugins/code-review/evals/README.md index 0245b4e..081a4a8 100644 --- a/plugins/code-review/evals/README.md +++ b/plugins/code-review/evals/README.md @@ -22,6 +22,7 @@ evals/ prompts/spec.txt # scanner brief — spec lens ({{spec}} carries the spec path) prompts/standards.txt # quality trigger with the standards fixture dir as repo root fixtures/ # inputs; fixtures/spec/ and fixtures/standards/ are multi-file + scope-mix/ # eval-19 input, kept out of fixtures/ so its paths classify by kind ``` Node dev deps (`@anthropic-ai/claude-agent-sdk` + `promptfoo`) and the run diff --git a/plugins/code-review/evals/promptfooconfig.yaml b/plugins/code-review/evals/promptfooconfig.yaml index c38ce59..f2cd5e6 100644 --- a/plugins/code-review/evals/promptfooconfig.yaml +++ b/plugins/code-review/evals/promptfooconfig.yaml @@ -782,7 +782,9 @@ tests: - description: 'eval-19 scope-classification (scope-mix/)' prompts: [quality-track] vars: - file: plugins/code-review/evals/fixtures/scope-mix + # Outside fixtures/: scope.md makes everything under a fixtures/ directory `test` kind, + # which would hide the source-vs-test classification this eval checks. + file: plugins/code-review/evals/scope-mix assert: - type: llm-rubric value: >- diff --git a/plugins/code-review/evals/fixtures/scope-mix/.github/workflows/ci.yml b/plugins/code-review/evals/scope-mix/.github/workflows/ci.yml similarity index 100% rename from plugins/code-review/evals/fixtures/scope-mix/.github/workflows/ci.yml rename to plugins/code-review/evals/scope-mix/.github/workflows/ci.yml diff --git a/plugins/code-review/evals/fixtures/scope-mix/build.mjs b/plugins/code-review/evals/scope-mix/build.mjs similarity index 100% rename from plugins/code-review/evals/fixtures/scope-mix/build.mjs rename to plugins/code-review/evals/scope-mix/build.mjs diff --git a/plugins/code-review/evals/fixtures/scope-mix/legacy-report.cjs b/plugins/code-review/evals/scope-mix/legacy-report.cjs similarity index 100% rename from plugins/code-review/evals/fixtures/scope-mix/legacy-report.cjs rename to plugins/code-review/evals/scope-mix/legacy-report.cjs diff --git a/plugins/code-review/evals/fixtures/scope-mix/notes.txt b/plugins/code-review/evals/scope-mix/notes.txt similarity index 100% rename from plugins/code-review/evals/fixtures/scope-mix/notes.txt rename to plugins/code-review/evals/scope-mix/notes.txt diff --git a/plugins/code-review/evals/fixtures/scope-mix/tests-e2e/checkout.spec.ts b/plugins/code-review/evals/scope-mix/tests-e2e/checkout.spec.ts similarity index 100% rename from plugins/code-review/evals/fixtures/scope-mix/tests-e2e/checkout.spec.ts rename to plugins/code-review/evals/scope-mix/tests-e2e/checkout.spec.ts From 3198e356936144d4ce917da24d4c961785066e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:12:27 +0200 Subject: [PATCH 31/44] docs(code-review): list the two new security rules and the spec offer in the docs --- .claude-plugin/marketplace.json | 2 +- plugins/code-review/.claude-plugin/plugin.json | 2 +- plugins/code-review/README.md | 8 +++++--- .../docs/adr/0002-active-lens-set-and-standards.md | 10 ++++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bb7b234..1eb004c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -117,7 +117,7 @@ { "name": "code-review", "source": "./plugins/code-review", - "description": "Unified code review that fans out parallel scanners over a change — one per active lens, six to eight per run — and merges them into one per-file report. Eight lenses: comment quality, four quality/craft lenses (readability & tests, naming & module, objects & patterns, simplicity & types), an always-on narrow security lens (secrets, injection, access checks, boundary validation, insecure settings), a performance lens for executable source (N+1, unbounded fetch, blocking-in-async, wasted renders), and a spec lens via --spec . Explicit rules in a root CODING_STANDARDS.md (plus a gitignored .local.md overlay) generate standards findings. The standalone comment-review and quality-review skills stay invocable for a single-lens pass. Reviews the current branch diff by default (or explicit paths / --base). Quality findings are tagged family · rule · severity; comment verdicts are R1–R12 · KEEP/REMOVE/REWRITE/MOVE/ADD, shown side by side and applied through a single risk-cut menu. Successor to the comment-review and quality-review plugins.", + "description": "Unified code review that fans out parallel scanners over a change — one per active lens, six to eight per run — and merges them into one per-file report. Eight lenses: comment quality, four quality/craft lenses (readability & tests, naming & module, objects & patterns, simplicity & types), an always-on narrow security lens (secrets, injection, access checks, boundary validation, insecure settings, IaC exposure, access widening), a performance lens for executable source (N+1, unbounded fetch, blocking-in-async, wasted renders), and a spec lens via --spec or a spec file the diff carries. Explicit rules in a root CODING_STANDARDS.md (plus a gitignored .local.md overlay) generate standards findings. The standalone comment-review and quality-review skills stay invocable for a single-lens pass. Reviews the current branch diff by default (or explicit paths / --base). Quality findings are tagged family · rule · severity; comment verdicts are R1–R12 · KEEP/REMOVE/REWRITE/MOVE/ADD, shown side by side and applied through a single risk-cut menu. Successor to the comment-review and quality-review plugins.", "version": "0.3.0", "author": { "name": "Mateusz Gostański", diff --git a/plugins/code-review/.claude-plugin/plugin.json b/plugins/code-review/.claude-plugin/plugin.json index bb33425..eb94ba8 100644 --- a/plugins/code-review/.claude-plugin/plugin.json +++ b/plugins/code-review/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "code-review", "version": "0.3.0", - "description": "Unified code review that fans out parallel scanners over a change — one per active lens, six to eight per run — and merges them into one per-file report. Eight lenses: comment quality, four quality/craft lenses (readability & tests, naming & module, objects & patterns, simplicity & types), an always-on narrow security lens (secrets, injection, access checks, boundary validation, insecure settings), a performance lens for executable source (N+1, unbounded fetch, blocking-in-async, wasted renders), and a spec lens via --spec . Explicit rules in a root CODING_STANDARDS.md (plus a gitignored .local.md overlay) generate standards findings. The standalone comment-review and quality-review skills stay invocable for a single-lens pass. Reviews the current branch diff by default (or explicit paths / --base). Quality findings are tagged family · rule · severity; comment verdicts are R1–R12 · KEEP/REMOVE/REWRITE/MOVE/ADD, shown side by side and applied through a single risk-cut menu. Successor to the comment-review and quality-review plugins.", + "description": "Unified code review that fans out parallel scanners over a change — one per active lens, six to eight per run — and merges them into one per-file report. Eight lenses: comment quality, four quality/craft lenses (readability & tests, naming & module, objects & patterns, simplicity & types), an always-on narrow security lens (secrets, injection, access checks, boundary validation, insecure settings, IaC exposure, access widening), a performance lens for executable source (N+1, unbounded fetch, blocking-in-async, wasted renders), and a spec lens via --spec or a spec file the diff carries. Explicit rules in a root CODING_STANDARDS.md (plus a gitignored .local.md overlay) generate standards findings. The standalone comment-review and quality-review skills stay invocable for a single-lens pass. Reviews the current branch diff by default (or explicit paths / --base). Quality findings are tagged family · rule · severity; comment verdicts are R1–R12 · KEEP/REMOVE/REWRITE/MOVE/ADD, shown side by side and applied through a single risk-cut menu. Successor to the comment-review and quality-review plugins.", "author": { "name": "Mateusz Gostański", "email": "mg@grixu.dev" diff --git a/plugins/code-review/README.md b/plugins/code-review/README.md index 7e7142d..f942264 100644 --- a/plugins/code-review/README.md +++ b/plugins/code-review/README.md @@ -80,8 +80,10 @@ Three more sit beyond the craft five — one always on, two gated — and the re says which ran: - **security** — always on. Secrets in source, injection sinks, missing access - checks, unvalidated boundaries, and insecure settings in source files. A - finding names both the source and the sink; a pattern alone is never a finding. + checks, unvalidated boundaries, insecure settings, infrastructure code that + exposes a secret or trusts too widely, and a change that relaxes an existing + authorization boundary. A finding names both the source and the sink; a pattern + alone is never a finding. - **performance** — only when the change touches executable source (not tests, not infrastructure-as-code, not `.sh`). N+1 calls, unbounded fetches, blocking calls on an async path, wasted React renders. Every finding names the multiplier, the call @@ -125,7 +127,7 @@ Tests, infrastructure-as-code, and `.sh` files are reviewed by the craft lenses **This is a craft review plus a narrow security lens, not a security audit.** The security lens looks for secrets, injection, access checks, boundary validation, -and insecure settings in source files; the performance lens raises diff-level +insecure settings, infrastructure exposure, and widened access in source files; the performance lens raises diff-level hypotheses it can point at a line. Neither is a dependency, config, or data-flow audit: `.env` files, manifests, and lockfiles are not scanned, and a vulnerability outside those shapes will surface only by accident. Do not read a diff --git a/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md b/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md index 758ef1a..6ad8a1e 100644 --- a/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md +++ b/plugins/code-review/docs/adr/0002-active-lens-set-and-standards.md @@ -8,12 +8,14 @@ dispatch: the five craft lenses (comments, readability & tests, naming & module, objects & patterns, simplicity & types) plus `security` are always active; `performance` is active only when the resolved files contain executable source (the `source` file kind — not tests, not infrastructure-as-code, not `.sh`); -`spec` is active only when the user passed `--spec `. The -orchestrator dispatches N scanners (6 to 8), waits for N `` blocks, +`spec` is active only when a spec resolves to a local file — from `--spec `, or from the spec-shaped file in the diff that `start-cr` offers and the +user accepts. The orchestrator dispatches N scanners (6 to 8), waits for N +`` blocks, applies fail-closed re-dispatch to every active lens, merges once all N have delivered, and records `Lenses: L of 8` in the tally with every inactive lens -and its reason. The user still picks no lens: the change and the `--spec` flag -decide. +and its reason. The user still picks no lens: the change, the `--spec` flag, +and the answer to that offer decide. Three lenses get their own rules files (`security.md`, `performance.md`, `spec.md`), read by `start-cr` only. Ten further rules from the same proposal — From 3cd82d923f764081aa02b8cf9915319ce0190df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 14:13:14 +0200 Subject: [PATCH 32/44] fix(fd3): read a declared gap as the deferred claim validate-spec records validate-spec buckets an owned, placed gap as deferred, so a ready verdict carrying a blocked claim was unreachable; split-to-tasks and the gap-rollout-spec fixture now use the same vocabulary. --- plugins/fd3/CHANGELOG.md | 6 +++--- .../evals/fixtures/gap-rollout-spec/DEFECTS.md | 13 +++++++------ .../gap-rollout-spec/spec/gap-rollout-spec.md | 2 +- .../fd3/evals/lib/checks/split-declared-gap.mjs | 8 ++++---- plugins/fd3/skills/split-to-tasks/SKILL.md | 17 +++++++++-------- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 681cdc0..e7023c8 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- `split-to-tasks` accepts a `ready` verdict that carries a declared gap: the blocked claim - becomes an operational task naming its owner, instead of stopping the split; an ownerless - blocked claim still stops it +- `split-to-tasks` turns each declared gap — a `deferred` claim with an owner and a placement — + into an operational task naming its owner, instead of stopping the split; a `blocked` claim, + which nothing owns, still stops it ### Added diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md index 3e31d3b..caa0871 100644 --- a/plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/DEFECTS.md @@ -1,8 +1,8 @@ # gap-rollout-spec — fixture contract This file is fixture documentation only. `reset-sandboxes.sh` excludes it from the sandbox copy. -`rollout-spec` with one change: its last verdict line carries a blocked claim that the spec itself -declares as a gap. It serves split-declared-gap. +`rollout-spec` with one change: its last verdict line carries a deferred claim — a gap the spec +itself declares with an owner and a placement. It serves split-declared-gap. ## The declared gap @@ -10,11 +10,12 @@ Section 7 names the unmeasured ledger write ceiling, its owner (**the platform t placement (**a gate before phase 2**). Section 12's `### Validation pass — 2026-07-30` block counts it, so the verdict line reads: -`Verdict: ready — claims: 1 verified / 0 deferred / 1 blocked — spec 224 lines at this verdict` +`Verdict: ready — claims: 1 verified / 1 deferred / 0 blocked — spec 224 lines at this verdict` -All three halves are load-bearing. `ready` with a blocked claim is what the precondition must -accept; the owner and the placement are what make it a declared gap rather than a stop; and `224` -equals `wc -l` on the spec, so any edit to the file must be followed by rewriting the number. +All three halves are load-bearing. `ready` with a deferred claim is what the precondition must +accept and turn into an operational task; the owner and the placement are what make it a declared +gap rather than a stop; and `224` equals `wc -l` on the spec, so any edit to the file must be +followed by rewriting the number. Removing the owner from section 7 turns this fixture into a stop-before-step-1 case and breaks the scenario — that case has its own fixture, `ownerless-gap-payments-spec`, on the validate side. diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md index a00d1b2..2c5396f 100644 --- a/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md @@ -215,7 +215,7 @@ pull request cites LED-100. ### Validation pass — 2026-07-30 -Verdict: ready — claims: 1 verified / 0 deferred / 1 blocked — spec 224 lines at this verdict +Verdict: ready — claims: 1 verified / 1 deferred / 0 blocked — spec 224 lines at this verdict | Claim | How it was verified | |---|---| diff --git a/plugins/fd3/evals/lib/checks/split-declared-gap.mjs b/plugins/fd3/evals/lib/checks/split-declared-gap.mjs index 8a1b035..b11d6e9 100644 --- a/plugins/fd3/evals/lib/checks/split-declared-gap.mjs +++ b/plugins/fd3/evals/lib/checks/split-declared-gap.mjs @@ -1,8 +1,8 @@ import * as h from '../helpers.mjs'; import * as s from './split-shared.mjs'; -// The spec's verdict line carries one blocked claim that the spec itself declares as a gap -// with an owner and a placement. The split proceeds and tracks the gap as an operational task. +// The spec's verdict line carries one deferred claim — a gap the spec itself declares with an +// owner and a placement. The split proceeds and tracks the gap as an operational task. export default (output) => { const c = h.checker(); const tasks = h.readTasks('split-declared-gap'); @@ -22,7 +22,7 @@ export default (output) => { c.check(/phase 2|ceiling|rate[-\s]?limit/i.test(note), `${gap.file}: the ## Note does not say what the gap is or where it lands`); } - // The run did not stop on the blocked claim: the files and the report exist. + // The run did not stop on the deferred claim: the files and the report exist. const SPLIT_REPORT = 'spec/gap-rollout-spec.split.md'; const diff = h.diffSandbox('split-declared-gap', 'gap-rollout-spec'); c.check(diff.added.includes(SPLIT_REPORT), `the split report ${SPLIT_REPORT} was not written beside the spec`); @@ -31,7 +31,7 @@ export default (output) => { c.check(stray.length === 0, `files created outside spec/tasks/: ${stray.join(', ')}`); const report = h.readSandboxFile('split-declared-gap', SPLIT_REPORT) || ''; - c.check(/1 blocked/.test(report) || /1 blocked/.test(output), 'neither the report nor the reply quotes the verdict line the split was taken against'); + c.check(/1 deferred/.test(report) || /1 deferred/.test(output), 'neither the report nor the reply quotes the verdict line the split was taken against'); return c.verdict(); }; diff --git a/plugins/fd3/skills/split-to-tasks/SKILL.md b/plugins/fd3/skills/split-to-tasks/SKILL.md index 952f86b..a6f12a5 100644 --- a/plugins/fd3/skills/split-to-tasks/SKILL.md +++ b/plugins/fd3/skills/split-to-tasks/SKILL.md @@ -40,16 +40,17 @@ that cites nothing — is a reason to stop and report it, never something to fix Splitting propagates the spec's defects into every task. A validation verdict in this conversation settles the question. Otherwise the spec must carry all three: read its evidence record **from the bottom** — the last verdict line in the file is the current one, position -decides and not the date — that line's blocked claims, if it carries any, are declared gaps, -and its count equals `wc -l` on the spec. +decides and not the date — that line reads `ready`, it counts no blocked claim, and its count +equals `wc -l` on the spec. -**A declared gap is work, not a stop.** A `ready` verdict may carry a blocked claim when -validation recorded it as a declared gap: the fact is unresolved and the spec names who resolves -it. Such a gap gets an **operational task** of its own, whose `## Note` says what has to come -back and from whom, and every task the gap blocks from being *written* draws a `depends-on` edge -onto it (step 4's authorship rule). A blocked claim with no named owner is not a declared gap. +**A declared gap is work, not a stop.** Validation records a gap the spec declares with an owner +and a placement as a `deferred` claim, and a `ready` verdict may carry any number of them: the +fact is unresolved and the spec names who resolves it. Each such gap gets an **operational task** +of its own, whose `## Note` says what has to come back and from whom, and every task the gap +blocks from being *written* draws a `depends-on` edge onto it (step 4's authorship rule). A +`blocked` claim is a gap nothing owns — it is not a declared gap, and it stops the split. -Anything short of that — an ownerless blocked claim, a count that does not match, a dated block +Anything short of that — a blocked claim, a count that does not match, a dated block with no verdict line, no pass anywhere — is a stop before step 1. A dated heading over verified rows is not a verdict. Validating is not this skill's work, and no command is named for it: on *validate first* the split ends with nothing written. What lifts the stop is the user's answer, never your own — say From 92e28386684ac89c4cd61fa693a484478cec0d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 17:53:35 +0200 Subject: [PATCH 33/44] test(fd3): give API-1's ledger write an endpoint that API-2 builds The rollout fixtures had API-1 write through an internal ledger endpoint that nothing built or cited, and a split run rightly stopped on that coverage gap. --- .../gap-rollout-spec/spec/gap-rollout-spec.md | 20 +++++++++---------- .../orphan-rollout-spec/spec/rollout-spec.md | 20 +++++++++---------- .../spec/protected-path-spec.md | 20 +++++++++---------- .../evals/fixtures/rollout-spec/DEFECTS.md | 4 +++- .../rollout-spec/spec/rollout-spec.md | 20 +++++++++---------- .../spec/rollout-spec.md | 20 +++++++++---------- 6 files changed, 53 insertions(+), 51 deletions(-) diff --git a/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md b/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md index 2c5396f..30a8fc9 100644 --- a/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md +++ b/plugins/fd3/evals/fixtures/gap-rollout-spec/spec/gap-rollout-spec.md @@ -47,21 +47,21 @@ New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql` - Migrations are applied by CI in filename order and are irreversible once applied to the shared staging database, so this element must land on `main` before any code that writes to it. -### API-2 — ledger entries endpoint (ledger service) +### API-2 — ledger entries endpoints (ledger service) -`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`. +`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`, and `POST /ledger/entries` is added beside it. -- Request: `orderId` query parameter, required, non-empty string. -- Response: JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`. -- Errors: 400 on a missing or empty `orderId`; 200 with `[]` when no entries exist. +- Request: `GET` takes an `orderId` query parameter, required, non-empty string; `POST` takes a JSON body `{ orderId: string, amountMinor: number, direction: "debit" | "credit" }`. +- Response: `GET` returns a JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`; `POST` returns 201 with the stored entry. +- Errors: 400 on a missing or empty `orderId` (both methods) or a negative `amountMinor`; `GET` returns 200 with `[]` when no entries exist. - Auth: the existing internal service token middleware; the dashboard's token is already accepted. -- Limits: response capped at 500 entries, newest first. +- Limits: `GET` responses capped at 500 entries, newest first; `POST` writes exactly one row. ### API-1 — settlement write from checkout `postCharge` (`repo-a/services/checkout/src/api/charge.ts:6`) gains a settlement write: when -`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry via the ledger -service's internal write endpoint. +`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry through API-2's +`POST /ledger/entries`. - Fields: `orderId`, `amountMinor` from the charge body; `direction` fixed to `credit`. - Errors: a ledger write failure fails the charge with HTTP 502 (flag on); flag off, no write @@ -123,7 +123,7 @@ request. 1. **DB-1** — new: migration `0001_create_ledger_entries.sql` in `repo-a/services/ledger/migrations/`. 2. **API-2** — changed: replace the stub in `repo-a/services/ledger/src/api/entries.ts:7-10` with - the real query and the 400 guard. + the real query and the 400 guard, and add the `POST` handler beside it. ### repo-a — `services/checkout/` (team-checkout) @@ -174,7 +174,7 @@ the migration stays behind, unused (expand-only; removal is out of scope, LED-10 - **DB-1** — probe: `psql "$LEDGER_DATABASE_URL" -c "\d ledger_entries"` lists the five columns and the `(order_id, created_at)` index. Before the change: `did not find any relation`. - **API-2** — probe: `curl -s "ledger.internal/ledger/entries?orderId=o_1"` returns `[]` with 200; - omitting `orderId` returns 400. Before the change both return the stub's empty 200. + omitting `orderId` returns 400; a valid `POST` returns 201. Before the change both `GET`s return the stub's empty 200 and the `POST` 404s. - **API-1** — triggered: with the flag on in a test environment, post a charge; one `credit` row for the order appears in `ledger_entries`. - **UI-1** — triggered: render the panel in the dashboard's component preview against the mock diff --git a/plugins/fd3/evals/fixtures/orphan-rollout-spec/spec/rollout-spec.md b/plugins/fd3/evals/fixtures/orphan-rollout-spec/spec/rollout-spec.md index 0ad40be..9625432 100644 --- a/plugins/fd3/evals/fixtures/orphan-rollout-spec/spec/rollout-spec.md +++ b/plugins/fd3/evals/fixtures/orphan-rollout-spec/spec/rollout-spec.md @@ -47,15 +47,15 @@ New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql` - Migrations are applied by CI in filename order and are irreversible once applied to the shared staging database, so this element must land on `main` before any code that writes to it. -### API-2 — ledger entries endpoint (ledger service) +### API-2 — ledger entries endpoints (ledger service) -`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`. +`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`, and `POST /ledger/entries` is added beside it. -- Request: `orderId` query parameter, required, non-empty string. -- Response: JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`. -- Errors: 400 on a missing or empty `orderId`; 200 with `[]` when no entries exist. +- Request: `GET` takes an `orderId` query parameter, required, non-empty string; `POST` takes a JSON body `{ orderId: string, amountMinor: number, direction: "debit" | "credit" }`. +- Response: `GET` returns a JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`; `POST` returns 201 with the stored entry. +- Errors: 400 on a missing or empty `orderId` (both methods) or a negative `amountMinor`; `GET` returns 200 with `[]` when no entries exist. - Auth: the existing internal service token middleware; the dashboard's token is already accepted. -- Limits: response capped at 500 entries, newest first. +- Limits: `GET` responses capped at 500 entries, newest first; `POST` writes exactly one row. ### API-3 — ledger entry export (ledger service) @@ -70,8 +70,8 @@ New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql` ### API-1 — settlement write from checkout `postCharge` (`repo-a/services/checkout/src/api/charge.ts:6`) gains a settlement write: when -`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry via the ledger -service's internal write endpoint. +`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry through API-2's +`POST /ledger/entries`. - Fields: `orderId`, `amountMinor` from the charge body; `direction` fixed to `credit`. - Errors: a ledger write failure fails the charge with HTTP 502 (flag on); flag off, no write @@ -133,7 +133,7 @@ request. 1. **DB-1** — new: migration `0001_create_ledger_entries.sql` in `repo-a/services/ledger/migrations/`. 2. **API-2** — changed: replace the stub in `repo-a/services/ledger/src/api/entries.ts:7-10` with - the real query and the 400 guard. + the real query and the 400 guard, and add the `POST` handler beside it. ### repo-a — `services/checkout/` (team-checkout) @@ -177,7 +177,7 @@ the migration stays behind, unused (expand-only; removal is out of scope, LED-10 - **DB-1** — probe: `psql "$LEDGER_DATABASE_URL" -c "\d ledger_entries"` lists the five columns and the `(order_id, created_at)` index. Before the change: `did not find any relation`. - **API-2** — probe: `curl -s "ledger.internal/ledger/entries?orderId=o_1"` returns `[]` with 200; - omitting `orderId` returns 400. Before the change both return the stub's empty 200. + omitting `orderId` returns 400; a valid `POST` returns 201. Before the change both `GET`s return the stub's empty 200 and the `POST` 404s. - **API-1** — triggered: with the flag on in a test environment, post a charge; one `credit` row for the order appears in `ledger_entries`. - **UI-1** — triggered: render the panel in the dashboard's component preview against the mock diff --git a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md index a546b26..71bd016 100644 --- a/plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md +++ b/plugins/fd3/evals/fixtures/protected-path-rollout-spec/spec/protected-path-spec.md @@ -47,15 +47,15 @@ New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql` - Migrations are applied by CI in filename order and are irreversible once applied to the shared staging database, so this element must land on `main` before any code that writes to it. -### API-2 — ledger entries endpoint (ledger service) +### API-2 — ledger entries endpoints (ledger service) -`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`. +`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`, and `POST /ledger/entries` is added beside it. -- Request: `orderId` query parameter, required, non-empty string. -- Response: JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`. -- Errors: 400 on a missing or empty `orderId`; 200 with `[]` when no entries exist. +- Request: `GET` takes an `orderId` query parameter, required, non-empty string; `POST` takes a JSON body `{ orderId: string, amountMinor: number, direction: "debit" | "credit" }`. +- Response: `GET` returns a JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`; `POST` returns 201 with the stored entry. +- Errors: 400 on a missing or empty `orderId` (both methods) or a negative `amountMinor`; `GET` returns 200 with `[]` when no entries exist. - Auth: the existing internal service token middleware; the dashboard's token is already accepted. -- Limits: response capped at 500 entries, newest first. +- Limits: `GET` responses capped at 500 entries, newest first; `POST` writes exactly one row. ### CI-1 — migration step in the deploy workflow @@ -71,8 +71,8 @@ hand. ### API-1 — settlement write from checkout `postCharge` (`repo-a/services/checkout/src/api/charge.ts:6`) gains a settlement write: when -`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry via the ledger -service's internal write endpoint. +`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry through API-2's +`POST /ledger/entries`. - Fields: `orderId`, `amountMinor` from the charge body; `direction` fixed to `credit`. - Errors: a ledger write failure fails the charge with HTTP 502 (flag on); flag off, no write @@ -135,7 +135,7 @@ request. 1. **DB-1** — new: migration `0001_create_ledger_entries.sql` in `repo-a/services/ledger/migrations/`. 2. **API-2** — changed: replace the stub in `repo-a/services/ledger/src/api/entries.ts:7-10` with - the real query and the 400 guard. + the real query and the 400 guard, and add the `POST` handler beside it. ### repo-a — `.github/workflows/` (release-team) @@ -185,7 +185,7 @@ the migration stays behind, unused (expand-only; removal is out of scope, LED-10 - **DB-1** — probe: `psql "$LEDGER_DATABASE_URL" -c "\d ledger_entries"` lists the five columns and the `(order_id, created_at)` index. Before the change: `did not find any relation`. - **API-2** — probe: `curl -s "ledger.internal/ledger/entries?orderId=o_1"` returns `[]` with 200; - omitting `orderId` returns 400. Before the change both return the stub's empty 200. + omitting `orderId` returns 400; a valid `POST` returns 201. Before the change both `GET`s return the stub's empty 200 and the `POST` 404s. - **CI-1** — probe: `rg "migrate" repo-a/.github/workflows/deploy.yml` shows the migration step above the deploy step. Before the change the file has no migration step. - **API-1** — triggered: with the flag on in a test environment, post a charge; one `credit` row diff --git a/plugins/fd3/evals/fixtures/rollout-spec/DEFECTS.md b/plugins/fd3/evals/fixtures/rollout-spec/DEFECTS.md index 39f615b..0895735 100644 --- a/plugins/fd3/evals/fixtures/rollout-spec/DEFECTS.md +++ b/plugins/fd3/evals/fixtures/rollout-spec/DEFECTS.md @@ -13,7 +13,9 @@ Exactly six tasks, exercising all four cut boundaries at once: (irreversibility cuts, moves to the front). 2. **API-2** — repo-a / `services/ledger`, phase 1, depends on the DB-1 task. 3. **API-1** — repo-a / `services/checkout`, phase 1, depends on the API-2 task (build order: - DB-1 → API-2 → API-1; monorepo ownership cuts API-1 away from API-2). + DB-1 → API-2 → API-1; monorepo ownership cuts API-1 away from API-2). API-1 writes through + API-2's `POST /ledger/entries`, and that endpoint must stay in API-2's contract and work + item: without it the write path has no builder, and the split stops on a coverage gap. 4. **UI-1** — repo-b, phase 1, no depends-on (repository cuts). 5. **CONFIG-1** — repo-a / `services/checkout`, phase 2 (phase cuts CONFIG-1 away from API-1). 6. **INTEGRATION-1** — repo-b, phase 2 (phase cuts INTEGRATION-1 away from UI-1). diff --git a/plugins/fd3/evals/fixtures/rollout-spec/spec/rollout-spec.md b/plugins/fd3/evals/fixtures/rollout-spec/spec/rollout-spec.md index 16cc498..5a126fd 100644 --- a/plugins/fd3/evals/fixtures/rollout-spec/spec/rollout-spec.md +++ b/plugins/fd3/evals/fixtures/rollout-spec/spec/rollout-spec.md @@ -47,21 +47,21 @@ New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql` - Migrations are applied by CI in filename order and are irreversible once applied to the shared staging database, so this element must land on `main` before any code that writes to it. -### API-2 — ledger entries endpoint (ledger service) +### API-2 — ledger entries endpoints (ledger service) -`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`. +`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`, and `POST /ledger/entries` is added beside it. -- Request: `orderId` query parameter, required, non-empty string. -- Response: JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`. -- Errors: 400 on a missing or empty `orderId`; 200 with `[]` when no entries exist. +- Request: `GET` takes an `orderId` query parameter, required, non-empty string; `POST` takes a JSON body `{ orderId: string, amountMinor: number, direction: "debit" | "credit" }`. +- Response: `GET` returns a JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`; `POST` returns 201 with the stored entry. +- Errors: 400 on a missing or empty `orderId` (both methods) or a negative `amountMinor`; `GET` returns 200 with `[]` when no entries exist. - Auth: the existing internal service token middleware; the dashboard's token is already accepted. -- Limits: response capped at 500 entries, newest first. +- Limits: `GET` responses capped at 500 entries, newest first; `POST` writes exactly one row. ### API-1 — settlement write from checkout `postCharge` (`repo-a/services/checkout/src/api/charge.ts:6`) gains a settlement write: when -`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry via the ledger -service's internal write endpoint. +`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry through API-2's +`POST /ledger/entries`. - Fields: `orderId`, `amountMinor` from the charge body; `direction` fixed to `credit`. - Errors: a ledger write failure fails the charge with HTTP 502 (flag on); flag off, no write @@ -123,7 +123,7 @@ request. 1. **DB-1** — new: migration `0001_create_ledger_entries.sql` in `repo-a/services/ledger/migrations/`. 2. **API-2** — changed: replace the stub in `repo-a/services/ledger/src/api/entries.ts:7-10` with - the real query and the 400 guard. + the real query and the 400 guard, and add the `POST` handler beside it. ### repo-a — `services/checkout/` (team-checkout) @@ -167,7 +167,7 @@ the migration stays behind, unused (expand-only; removal is out of scope, LED-10 - **DB-1** — probe: `psql "$LEDGER_DATABASE_URL" -c "\d ledger_entries"` lists the five columns and the `(order_id, created_at)` index. Before the change: `did not find any relation`. - **API-2** — probe: `curl -s "ledger.internal/ledger/entries?orderId=o_1"` returns `[]` with 200; - omitting `orderId` returns 400. Before the change both return the stub's empty 200. + omitting `orderId` returns 400; a valid `POST` returns 201. Before the change both `GET`s return the stub's empty 200 and the `POST` 404s. - **API-1** — triggered: with the flag on in a test environment, post a charge; one `credit` row for the order appears in `ledger_entries`. - **UI-1** — triggered: render the panel in the dashboard's component preview against the mock diff --git a/plugins/fd3/evals/fixtures/unvalidated-rollout-spec/spec/rollout-spec.md b/plugins/fd3/evals/fixtures/unvalidated-rollout-spec/spec/rollout-spec.md index 5718a9b..06d707b 100644 --- a/plugins/fd3/evals/fixtures/unvalidated-rollout-spec/spec/rollout-spec.md +++ b/plugins/fd3/evals/fixtures/unvalidated-rollout-spec/spec/rollout-spec.md @@ -47,21 +47,21 @@ New migration `repo-a/services/ledger/migrations/0001_create_ledger_entries.sql` - Migrations are applied by CI in filename order and are irreversible once applied to the shared staging database, so this element must land on `main` before any code that writes to it. -### API-2 — ledger entries endpoint (ledger service) +### API-2 — ledger entries endpoints (ledger service) -`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`. +`GET /ledger/entries?orderId=` replaces the stub at `repo-a/services/ledger/src/api/entries.ts:7`, and `POST /ledger/entries` is added beside it. -- Request: `orderId` query parameter, required, non-empty string. -- Response: JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`. -- Errors: 400 on a missing or empty `orderId`; 200 with `[]` when no entries exist. +- Request: `GET` takes an `orderId` query parameter, required, non-empty string; `POST` takes a JSON body `{ orderId: string, amountMinor: number, direction: "debit" | "credit" }`. +- Response: `GET` returns a JSON array of `{ orderId: string, amountMinor: number, direction: "debit" | "credit", createdAt: string }`; `POST` returns 201 with the stored entry. +- Errors: 400 on a missing or empty `orderId` (both methods) or a negative `amountMinor`; `GET` returns 200 with `[]` when no entries exist. - Auth: the existing internal service token middleware; the dashboard's token is already accepted. -- Limits: response capped at 500 entries, newest first. +- Limits: `GET` responses capped at 500 entries, newest first; `POST` writes exactly one row. ### API-1 — settlement write from checkout `postCharge` (`repo-a/services/checkout/src/api/charge.ts:6`) gains a settlement write: when -`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry via the ledger -service's internal write endpoint. +`flags.asyncSettlement` is true, an accepted charge writes one `credit` entry through API-2's +`POST /ledger/entries`. - Fields: `orderId`, `amountMinor` from the charge body; `direction` fixed to `credit`. - Errors: a ledger write failure fails the charge with HTTP 502 (flag on); flag off, no write @@ -123,7 +123,7 @@ request. 1. **DB-1** — new: migration `0001_create_ledger_entries.sql` in `repo-a/services/ledger/migrations/`. 2. **API-2** — changed: replace the stub in `repo-a/services/ledger/src/api/entries.ts:7-10` with - the real query and the 400 guard. + the real query and the 400 guard, and add the `POST` handler beside it. ### repo-a — `services/checkout/` (team-checkout) @@ -167,7 +167,7 @@ the migration stays behind, unused (expand-only; removal is out of scope, LED-10 - **DB-1** — probe: `psql "$LEDGER_DATABASE_URL" -c "\d ledger_entries"` lists the five columns and the `(order_id, created_at)` index. Before the change: `did not find any relation`. - **API-2** — probe: `curl -s "ledger.internal/ledger/entries?orderId=o_1"` returns `[]` with 200; - omitting `orderId` returns 400. Before the change both return the stub's empty 200. + omitting `orderId` returns 400; a valid `POST` returns 201. Before the change both `GET`s return the stub's empty 200 and the `POST` 404s. - **API-1** — triggered: with the flag on in a test environment, post a charge; one `credit` row for the order appears in `ledger_entries`. - **UI-1** — triggered: render the panel in the dashboard's component preview against the mock From 8acc50d72df854294b525aa61efb6d777fadb0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 17:54:57 +0200 Subject: [PATCH 34/44] test(fd3): back the defective spec's clean claims with the files they cite The fixture's prerequisites, apply mechanism and delivery path pointed at a manifest, a /metrics endpoint, a CI deploy and queue wiring the repository never had, so a faithful validation raised them next to the five planted defects. --- .../.github/workflows/deploy.yml | 16 +++++++++ .../defective-payments-spec/DEFECTS.md | 13 ++++++++ .../defective-payments-spec/README.md | 5 ++- .../deploy/manifest.yaml | 33 +++++++++++++++++++ .../fixtures/charge.json | 1 + .../migrations/0001_create_orders.sql | 5 +++ .../defective-payments-spec/package.json | 8 +++++ .../defective-payments-spec/src/db.ts | 3 ++ .../src/http/merchantAuth.ts | 6 ++++ .../defective-payments-spec/src/metrics.ts | 9 +++++ .../src/orders/repository.ts | 6 ++++ .../src/queue/poller.ts | 12 +++++++ .../src/queue/worker.ts | 9 +++-- .../defective-payments-spec/src/server.ts | 29 ++++++++++++++++ .../src/webhooks/enqueue.ts | 4 +++ 15 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/.github/workflows/deploy.yml create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/deploy/manifest.yaml create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/fixtures/charge.json create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/migrations/0001_create_orders.sql create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/src/db.ts create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/src/http/merchantAuth.ts create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/src/metrics.ts create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/src/orders/repository.ts create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/poller.ts create mode 100644 plugins/fd3/evals/fixtures/defective-payments-spec/src/server.ts diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/.github/workflows/deploy.yml b/plugins/fd3/evals/fixtures/defective-payments-spec/.github/workflows/deploy.yml new file mode 100644 index 0000000..4de4536 --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/.github/workflows/deploy.yml @@ -0,0 +1,16 @@ +name: deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm ci && npm test + - run: npx node-pg-migrate up + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + - run: kubectl apply -f deploy/manifest.yaml diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/DEFECTS.md b/plugins/fd3/evals/fixtures/defective-payments-spec/DEFECTS.md index f7bfa9a..9f81177 100644 --- a/plugins/fd3/evals/fixtures/defective-payments-spec/DEFECTS.md +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/DEFECTS.md @@ -33,6 +33,19 @@ without re-checking this list breaks the eval. - `src/store/idempotency.ts` — `new Map` at line 3; functions at lines 5 and 9. - `src/webhooks/enqueue.ts` — `enqueueWebhook` at line 8. +## The repository backs every claim the spec does not plant as a defect + +A run that finds nothing wrong with the prerequisites, the apply mechanism or the delivery path +must be right to do so, or the five defects drown in real findings. So the repository carries: +`DATABASE_URL`, the merchant key and the webhook URL in `deploy/manifest.yaml`, plus its scrape +annotations for `/metrics`; the orders schema (`migrations/0001_create_orders.sql`, +`src/orders/repository.ts`) and the `pg` client; `src/server.ts` serving `POST /charges` behind +`src/http/merchantAuth.ts` (402 on decline) and `GET /metrics`; `src/queue/poller.ts` draining +the queue into `deliver`; a `post` that really calls the merchant endpoint, so a failing merchant is +reachable; `fixtures/charge.json` for the API-1 probe; and `.github/workflows/deploy.yml`, which +migrates and deploys on merge to `main`. Removing any of these turns a clean claim into a sixth +finding. + Everything else in the spec is deliberately clean: all other citations resolve, the decision table is otherwise consistent, every other element carries a code, the evidence table exists and its other rows are true. Section 3 carries a **risks accepted** table and section 7's rollout table diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/README.md b/plugins/fd3/evals/fixtures/defective-payments-spec/README.md index 1fe4ce4..c903487 100644 --- a/plugins/fd3/evals/fixtures/defective-payments-spec/README.md +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/README.md @@ -2,7 +2,10 @@ Charges cards and delivers `charge.settled` webhooks to merchants. +- `src/server.ts` — HTTP entry point: `POST /charges` behind the merchant API key, `GET /metrics` - `src/billing/` — charge entry point and idempotency handling - `src/webhooks/` — event enqueueing -- `src/queue/` — the delivery worker +- `src/queue/` — the delivery worker and the poller that drains the queue into it - `src/store/` — idempotency key storage (in-memory today) +- `src/orders/`, `src/db.ts`, `migrations/` — the orders schema in Postgres +- `deploy/manifest.yaml`, `.github/workflows/deploy.yml` — CI migrates and deploys on merge to `main` diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/deploy/manifest.yaml b/plugins/fd3/evals/fixtures/defective-payments-spec/deploy/manifest.yaml new file mode 100644 index 0000000..92ae0fd --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/deploy/manifest.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: payments-service +spec: + replicas: 2 + selector: + matchLabels: + app: payments-service + template: + metadata: + labels: + app: payments-service + annotations: + prometheus.io/scrape: "true" + prometheus.io/path: /metrics + prometheus.io/port: "3000" + spec: + containers: + - name: payments-service + image: registry.internal/payments-service:latest + ports: + - containerPort: 3000 + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: { name: payments-db, key: url } + - name: MERCHANT_API_KEY + valueFrom: + secretKeyRef: { name: payments-merchant, key: api-key } + - name: MERCHANT_WEBHOOK_URL + valueFrom: + configMapKeyRef: { name: payments-config, key: merchant-webhook-url } diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/fixtures/charge.json b/plugins/fd3/evals/fixtures/defective-payments-spec/fixtures/charge.json new file mode 100644 index 0000000..dc413dc --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/fixtures/charge.json @@ -0,0 +1 @@ +{ "orderId": "o_1", "amountMinor": 1200, "currency": "EUR" } diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/migrations/0001_create_orders.sql b/plugins/fd3/evals/fixtures/defective-payments-spec/migrations/0001_create_orders.sql new file mode 100644 index 0000000..33cb239 --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/migrations/0001_create_orders.sql @@ -0,0 +1,5 @@ +CREATE TABLE orders ( + id TEXT PRIMARY KEY, + amount_minor BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/package.json b/plugins/fd3/evals/fixtures/defective-payments-spec/package.json index 6f439aa..6bec7fb 100644 --- a/plugins/fd3/evals/fixtures/defective-payments-spec/package.json +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/package.json @@ -3,6 +3,14 @@ "version": "1.4.2", "private": true, "scripts": { + "start": "node dist/server.js", "test": "vitest run" + }, + "dependencies": { + "pg": "^8.12.0" + }, + "devDependencies": { + "node-pg-migrate": "^7.6.0", + "vitest": "^2.1.0" } } diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/db.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/db.ts new file mode 100644 index 0000000..7446fda --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/db.ts @@ -0,0 +1,3 @@ +import { Pool } from "pg"; + +export const pool = new Pool({ connectionString: process.env.DATABASE_URL }); diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/http/merchantAuth.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/http/merchantAuth.ts new file mode 100644 index 0000000..caa3e1b --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/http/merchantAuth.ts @@ -0,0 +1,6 @@ +import type { IncomingMessage } from "node:http"; + +export function isMerchantAuthorized(req: IncomingMessage): boolean { + const key = req.headers["x-api-key"]; + return typeof key === "string" && key === process.env.MERCHANT_API_KEY; +} diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/metrics.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/metrics.ts new file mode 100644 index 0000000..63e73a3 --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/metrics.ts @@ -0,0 +1,9 @@ +let chargesTotal = 0; + +export function countCharge(): void { + chargesTotal += 1; +} + +export function renderMetrics(): string { + return `# TYPE charges_total counter\ncharges_total ${chargesTotal}\n`; +} diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/orders/repository.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/orders/repository.ts new file mode 100644 index 0000000..4737d9c --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/orders/repository.ts @@ -0,0 +1,6 @@ +import { pool } from "../db"; + +export async function findOrder(orderId: string) { + const { rows } = await pool.query("SELECT id, amount_minor FROM orders WHERE id = $1", [orderId]); + return rows[0]; +} diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/poller.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/poller.ts new file mode 100644 index 0000000..a9798a5 --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/poller.ts @@ -0,0 +1,12 @@ +import { drainQueue } from "../webhooks/enqueue"; +import { deliver } from "./worker"; + +const POLL_INTERVAL_MS = 1000; + +export function startDeliveryPoller(): NodeJS.Timeout { + return setInterval(async () => { + for (const event of drainQueue()) { + await deliver(event); + } + }, POLL_INTERVAL_MS); +} diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/worker.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/worker.ts index fe0fb22..42efc3b 100644 --- a/plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/worker.ts +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/queue/worker.ts @@ -11,6 +11,11 @@ export async function deliver(event: WebhookEvent): Promise { } } -async function post(_event: WebhookEvent): Promise { - return true; +async function post(event: WebhookEvent): Promise { + const res = await fetch(process.env.MERCHANT_WEBHOOK_URL ?? "", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(event), + }); + return res.ok; } diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/server.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/server.ts new file mode 100644 index 0000000..b9505f7 --- /dev/null +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/server.ts @@ -0,0 +1,29 @@ +import { createServer } from "node:http"; +import { charge, type ChargeRequest } from "./billing/charge"; +import { isMerchantAuthorized } from "./http/merchantAuth"; +import { countCharge, renderMetrics } from "./metrics"; +import { startDeliveryPoller } from "./queue/poller"; + +const server = createServer(async (req, res) => { + if (req.method === "GET" && req.url === "/metrics") { + res.writeHead(200, { "content-type": "text/plain" }).end(renderMetrics()); + return; + } + if (req.method === "POST" && req.url === "/charges") { + if (!isMerchantAuthorized(req)) { + res.writeHead(401).end(); + return; + } + let body = ""; + for await (const chunk of req) body += chunk; + const result = await charge(JSON.parse(body) as ChargeRequest); + countCharge(); + res.writeHead(result.status === "declined" ? 402 : 200, { "content-type": "application/json" }); + res.end(JSON.stringify(result)); + return; + } + res.writeHead(404).end(); +}); + +startDeliveryPoller(); +server.listen(Number(process.env.PORT ?? 3000)); diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/src/webhooks/enqueue.ts b/plugins/fd3/evals/fixtures/defective-payments-spec/src/webhooks/enqueue.ts index 2bf2336..fd2d828 100644 --- a/plugins/fd3/evals/fixtures/defective-payments-spec/src/webhooks/enqueue.ts +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/src/webhooks/enqueue.ts @@ -8,3 +8,7 @@ const queue: WebhookEvent[] = []; export async function enqueueWebhook(type: string, payload: unknown): Promise { queue.push({ type, payload }); } + +export function drainQueue(): WebhookEvent[] { + return queue.splice(0); +} From bdd125dc6806c0abab2f71ed3ae1818ed1bb88da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:04:38 +0200 Subject: [PATCH 35/44] fix(fd3): give the split table all six columns in the reply --- plugins/fd3/skills/split-to-tasks/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/fd3/skills/split-to-tasks/SKILL.md b/plugins/fd3/skills/split-to-tasks/SKILL.md index a6f12a5..1613bf9 100644 --- a/plugins/fd3/skills/split-to-tasks/SKILL.md +++ b/plugins/fd3/skills/split-to-tasks/SKILL.md @@ -292,6 +292,6 @@ a task-file glob trips over it. It carries one table (slug, repository, branch, depends-on, elements), the branch creation order and stack chain per repository, where the files went, the coverage statement from step 5, every work item split across tasks with its seam, any size-check warning, the verdict line this split was taken against quoted verbatim, -and anything the user still owes an answer. In the conversation give the path and the table, -not the file. Anything in the report that binds one task's work also goes into that task's +and anything the user still owes an answer. In the conversation give the path and the same +table — all six columns, `elements` included — not the file. Anything in the report that binds one task's work also goes into that task's `## Note`, in the imperative — the conversation ends before implementation starts. From 819532d83f75c38ca64d78e952ff817c31f99cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:04:38 +0200 Subject: [PATCH 36/44] fix(fd3): post the unblocked questions instead of holding a round for a lookup --- plugins/fd3/skills/grill-topic/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/fd3/skills/grill-topic/SKILL.md b/plugins/fd3/skills/grill-topic/SKILL.md index 4c41cb2..21bedca 100644 --- a/plugins/fd3/skills/grill-topic/SKILL.md +++ b/plugins/fd3/skills/grill-topic/SKILL.md @@ -77,7 +77,7 @@ Route every lookup by where the fact lives — the routes and dispatch rules are The session's research directory is `research/` in the session scratchpad. Dispatch prompts name it, agents write their full reports there, and what enters this conversation is each report's condensed answers and its file path. When a round argues from a report's findings, cite the file — the user can open the evidence. -A question you dispatched a lookup for is **blocked by that lookup** — no exceptions. Do not predict what the lookup will return, or which questions it will turn out to touch: whether a fact changes a question is knowable only once you hold the fact. Questions you sent nobody to answer are not blocked — ask those now; a running lookup is an unsettled prerequisite for its own question only. +A question you dispatched a lookup for is **blocked by that lookup** — no exceptions. Do not predict what the lookup will return, or which questions it will turn out to touch: whether a fact changes a question is knowable only once you hold the fact. Questions you sent nobody to answer are not blocked — ask those now; a running lookup is an unsettled prerequisite for its own question only. Never hold a round back to keep it whole: post the unblocked questions and name the blocked numbers on the round's opening line. A turn that ends on "waiting for the lookup" leaves the user nothing to answer. A recommendation is never conditional. If you would write "recommended, provided the check confirms it", the question is blocked by that check and stays out of the round — a conditional recommendation gets answered as an unconditional one. The same bar holds inside an option's cost, its preamble and the recommendation itself: any admission that something outside this conversation is unchecked — a source unread, a contradiction unresolved, a behaviour unobserved — is a conditional recommendation wearing a cost's clothes, and the question is blocked by that lookup. If you find yourself writing the hedge, you have found the dispatch. From b58c1371acf383d0ad637dcf06efcfe64e2e913c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:04:48 +0200 Subject: [PATCH 37/44] docs(fd3): note the split-table and round-holding fixes in the changelog --- plugins/fd3/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index e7023c8..11d7a9e 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `split-to-tasks` turns each declared gap — a `deferred` claim with an owner and a placement — into an operational task naming its owner, instead of stopping the split; a `blocked` claim, which nothing owns, still stops it +- `split-to-tasks` shows the full six-column table in its reply, `elements` included +- `grill-topic` posts a round's unblocked questions while a lookup runs, instead of holding the + whole round and ending the turn on "waiting" ### Added From 38688e38263acf3af2b4465c09831a563d9a9b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:05:04 +0200 Subject: [PATCH 38/44] test(fd3): send the merchant key in the defective spec's API-1 probe --- .../fixtures/defective-payments-spec/spec/payments-spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/fd3/evals/fixtures/defective-payments-spec/spec/payments-spec.md b/plugins/fd3/evals/fixtures/defective-payments-spec/spec/payments-spec.md index 1a7f4ad..681386a 100644 --- a/plugins/fd3/evals/fixtures/defective-payments-spec/spec/payments-spec.md +++ b/plugins/fd3/evals/fixtures/defective-payments-spec/spec/payments-spec.md @@ -127,7 +127,7 @@ dropping it is cleanup (section 9), not rollback. - **DB-1** — probe: `psql "$DATABASE_URL" -c "\d idempotency_keys"` lists the four columns. Before the change the same command errors with `did not find any relation`. -- **API-1** — probe: `curl -s -X POST localhost:3000/charges -d @fixtures/charge.json | jq .deliveryStatus` +- **API-1** — probe: `curl -s -X POST -H "x-api-key: $MERCHANT_API_KEY" localhost:3000/charges -d @fixtures/charge.json | jq .deliveryStatus` prints `"queued"`. Before the change it prints `null`. - **Delivery retry worker** — triggered: post a charge with the mock merchant endpoint returning 500; the outcome table gains 5 rows for the event, `delivered = false` on each. From 2ad21ea70a59877944c1c4785c97a90f2dcc281a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:14:58 +0200 Subject: [PATCH 39/44] fix(fd3): make the first tool call in the reply that posts the split checklist --- plugins/fd3/CHANGELOG.md | 2 ++ plugins/fd3/skills/split-to-tasks/SKILL.md | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index 11d7a9e..f825a4e 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 into an operational task naming its owner, instead of stopping the split; a `blocked` claim, which nothing owns, still stops it - `split-to-tasks` shows the full six-column table in its reply, `elements` included +- `split-to-tasks` makes its first tool call in the same reply as the opening checklist — a reply + that only announced the checklist ended a headless run with nothing done - `grill-topic` posts a round's unblocked questions while a lookup runs, instead of holding the whole round and ending the turn on "waiting" diff --git a/plugins/fd3/skills/split-to-tasks/SKILL.md b/plugins/fd3/skills/split-to-tasks/SKILL.md index 1613bf9..71f32bb 100644 --- a/plugins/fd3/skills/split-to-tasks/SKILL.md +++ b/plugins/fd3/skills/split-to-tasks/SKILL.md @@ -59,10 +59,11 @@ ends the run says what the record held and which way the user answered. ## Workflow -Post this checklist as your first message in the run, before any tool call — a run that then stops -on an unresolvable path has cost one message. Post it again in full — marks updated, never -compressed to a line and never summarised — before every user interaction (the question batch, the -report) and at the close: +Open your first reply with this checklist, before any tool call — a run that then stops on an +unresolvable path has cost one message — and make the first tool call in that same reply. A reply +that only announces the checklist, or only posts it, ends the turn with nothing done. Post it again +in full — marks updated, never compressed to a line and never summarised — before every user +interaction (the question batch, the report) and at the close: ``` - [ ] 1. Enumerate: work items, element codes, phases and gates, ownership, tickets From 2977b86c6fdbbccb2bb24f0876d8506d76d6b8d3 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:17:39 +0000 Subject: [PATCH 40/44] fix(fd3): make the first tool call in implement-tasks' checklist reply too 2ad21ea fixed split-to-tasks; implement-tasks carried the identical wording and has no eval scenario that would catch it. --- plugins/fd3/CHANGELOG.md | 4 ++-- plugins/fd3/skills/implement-tasks/SKILL.md | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/fd3/CHANGELOG.md b/plugins/fd3/CHANGELOG.md index f825a4e..64e1b3a 100644 --- a/plugins/fd3/CHANGELOG.md +++ b/plugins/fd3/CHANGELOG.md @@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 into an operational task naming its owner, instead of stopping the split; a `blocked` claim, which nothing owns, still stops it - `split-to-tasks` shows the full six-column table in its reply, `elements` included -- `split-to-tasks` makes its first tool call in the same reply as the opening checklist — a reply - that only announced the checklist ended a headless run with nothing done +- `split-to-tasks` and `implement-tasks` make their first tool call in the same reply as the opening + checklist — a reply that only announced the checklist ended a headless run with nothing done - `grill-topic` posts a round's unblocked questions while a lookup runs, instead of holding the whole round and ending the turn on "waiting" diff --git a/plugins/fd3/skills/implement-tasks/SKILL.md b/plugins/fd3/skills/implement-tasks/SKILL.md index ad8ced0..595f837 100644 --- a/plugins/fd3/skills/implement-tasks/SKILL.md +++ b/plugins/fd3/skills/implement-tasks/SKILL.md @@ -33,10 +33,11 @@ to have. ## Workflow -Post this checklist as your first message in the run, before any tool call — a run that then stops -on an unresolvable path has cost one message. Post it again in full — marks updated, never -compressed to a line and never summarised — before every user interaction (the question batch, each -report round) and at the close: +Open your first reply with this checklist, before any tool call — a run that then stops on an +unresolvable path has cost one message — and make the first tool call in that same reply. A reply +that only announces the checklist, or only posts it, ends the turn with nothing done. Post it again +in full — marks updated, never compressed to a line and never summarised — before every user +interaction (the question batch, each report round) and at the close: ``` - [ ] 1. Read the graph: parse task frontmatter, resolve repositories, check integrity From dec9ad5bd994d931909a0b246505bc96157a9d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:29:13 +0200 Subject: [PATCH 41/44] test(fd3): count a round asked through AskUserQuestion in the build-spec gate check The check read only the final message, so a run that put round 1 to the user through the tool, then ended waiting on a lookup, failed as if no grilling ran. --- plugins/fd3/evals/lib/checks/build-spec-gate.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/fd3/evals/lib/checks/build-spec-gate.mjs b/plugins/fd3/evals/lib/checks/build-spec-gate.mjs index 61099cb..3b3510b 100644 --- a/plugins/fd3/evals/lib/checks/build-spec-gate.mjs +++ b/plugins/fd3/evals/lib/checks/build-spec-gate.mjs @@ -1,10 +1,17 @@ import * as h from '../helpers.mjs'; -export default (output) => { +const NUMBERED = /^\s{0,3}(?:#{1,4}\s+)?(?:\*\*)?Q?\d+[.)]\s/m; + +export default (output, context) => { const c = h.checker(); - const numbered = output.match(/^\s{0,3}(?:#{1,4}\s+)?(?:\*\*)?Q?\d+[.)]\s/gm) || []; - c.check(numbered.length >= 1, 'no numbered round of questions — the grilling half never ran'); + // A round may go out through AskUserQuestion and get auto-answered, leaving the final message + // with no numbered question even though the grilling ran. + const asked = (context?.providerResponse?.metadata?.toolCalls || []) + .filter((call) => call.name === 'AskUserQuestion') + .flatMap((call) => call.input?.questions || []) + .some((q) => NUMBERED.test(q.question || '')); + c.check(NUMBERED.test(output) || asked, 'no numbered round of questions — the grilling half never ran'); // The gate: without a confirmed closing summary the write-spec half must not start. const diff = h.diffSandbox('build-spec-gate', 'retry-topic'); From ac6e253280cdb64ef320660be270e5cc1fe03f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:37:33 +0200 Subject: [PATCH 42/44] test(fd3): take any AskUserQuestion call as the gate's evidence that grilling ran --- plugins/fd3/evals/lib/checks/build-spec-gate.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/fd3/evals/lib/checks/build-spec-gate.mjs b/plugins/fd3/evals/lib/checks/build-spec-gate.mjs index 3b3510b..434392d 100644 --- a/plugins/fd3/evals/lib/checks/build-spec-gate.mjs +++ b/plugins/fd3/evals/lib/checks/build-spec-gate.mjs @@ -6,11 +6,10 @@ export default (output, context) => { const c = h.checker(); // A round may go out through AskUserQuestion and get auto-answered, leaving the final message - // with no numbered question even though the grilling ran. + // with no numbered question even though the grilling ran. Numbering is grill-numbered-questions' + // concern; here the call itself is the evidence. const asked = (context?.providerResponse?.metadata?.toolCalls || []) - .filter((call) => call.name === 'AskUserQuestion') - .flatMap((call) => call.input?.questions || []) - .some((q) => NUMBERED.test(q.question || '')); + .some((call) => call.name === 'AskUserQuestion'); c.check(NUMBERED.test(output) || asked, 'no numbered round of questions — the grilling half never ran'); // The gate: without a confirmed closing summary the write-spec half must not start. From d5d9dc4825c425915554a0d9202babbfab52d0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:37:33 +0200 Subject: [PATCH 43/44] test(fd3): say why the prior-conversation assert grades placement only --- plugins/fd3/evals/lib/checks/grill-session-files.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/fd3/evals/lib/checks/grill-session-files.mjs b/plugins/fd3/evals/lib/checks/grill-session-files.mjs index 88518ed..8758f21 100644 --- a/plugins/fd3/evals/lib/checks/grill-session-files.mjs +++ b/plugins/fd3/evals/lib/checks/grill-session-files.mjs @@ -20,6 +20,8 @@ export default (output) => { ); } + // Placement only: the prompt is the bare command, so nothing was established before it and + // writing no prior-conversation record is correct here. const prior = diff.added.filter((f) => /prior-conversation\.md$/.test(f)); c.check( prior.every((f) => /(^|\/)research\/prior-conversation\.md$/.test(f)), From 26056cf367121133611c69b9f282337e15f95357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gosta=C5=84ski?= Date: Wed, 23 Sep 2026 18:42:52 +0200 Subject: [PATCH 44/44] test(fd3): share the tool-round liveness check with grill-session-files --- plugins/fd3/evals/lib/checks/build-spec-gate.mjs | 8 ++------ plugins/fd3/evals/lib/checks/grill-session-files.mjs | 6 +++--- plugins/fd3/evals/lib/helpers.mjs | 6 ++++++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/plugins/fd3/evals/lib/checks/build-spec-gate.mjs b/plugins/fd3/evals/lib/checks/build-spec-gate.mjs index 434392d..74f3246 100644 --- a/plugins/fd3/evals/lib/checks/build-spec-gate.mjs +++ b/plugins/fd3/evals/lib/checks/build-spec-gate.mjs @@ -5,12 +5,8 @@ const NUMBERED = /^\s{0,3}(?:#{1,4}\s+)?(?:\*\*)?Q?\d+[.)]\s/m; export default (output, context) => { const c = h.checker(); - // A round may go out through AskUserQuestion and get auto-answered, leaving the final message - // with no numbered question even though the grilling ran. Numbering is grill-numbered-questions' - // concern; here the call itself is the evidence. - const asked = (context?.providerResponse?.metadata?.toolCalls || []) - .some((call) => call.name === 'AskUserQuestion'); - c.check(NUMBERED.test(output) || asked, 'no numbered round of questions — the grilling half never ran'); + // Numbering is grill-numbered-questions' concern; here the round only has to have gone out. + c.check(NUMBERED.test(output) || h.askedThroughTool(context), 'no round of questions — the grilling half never ran'); // The gate: without a confirmed closing summary the write-spec half must not start. const diff = h.diffSandbox('build-spec-gate', 'retry-topic'); diff --git a/plugins/fd3/evals/lib/checks/grill-session-files.mjs b/plugins/fd3/evals/lib/checks/grill-session-files.mjs index 8758f21..4e0a091 100644 --- a/plugins/fd3/evals/lib/checks/grill-session-files.mjs +++ b/plugins/fd3/evals/lib/checks/grill-session-files.mjs @@ -3,11 +3,11 @@ import * as h from '../helpers.mjs'; // The grilling half keeps two bookkeeping files, and both have a pinned home: the question // ledger under notes/, the prior-conversation record under research/. Loose in the working // tree they land in the user's repository and outlive the session. -export default (output) => { +export default (output, context) => { const c = h.checker(); - const numbered = output.match(/^\s{0,3}(?:#{1,4}\s+)?(?:\*\*)?Q?\d+[.)]\s/gm) || []; - c.check(numbered.length >= 1, 'no numbered round of questions — the grilling half never ran'); + const numbered = /^\s{0,3}(?:#{1,4}\s+)?(?:\*\*)?Q?\d+[.)]\s/m.test(output); + c.check(numbered || h.askedThroughTool(context), 'no round of questions — the grilling half never ran'); const diff = h.diffSandbox('grill-session-files', 'retry-topic'); diff --git a/plugins/fd3/evals/lib/helpers.mjs b/plugins/fd3/evals/lib/helpers.mjs index 3c8c1de..f63a0a7 100644 --- a/plugins/fd3/evals/lib/helpers.mjs +++ b/plugins/fd3/evals/lib/helpers.mjs @@ -160,3 +160,9 @@ export function section(output, heading) { const next = /^##\s+/m.exec(rest); return next ? rest.slice(0, next.index) : rest; } + +// A round that went out through AskUserQuestion is auto-answered under `first_option`, so the +// final message can hold no numbered question even though the grilling ran. +export function askedThroughTool(context) { + return (context?.providerResponse?.metadata?.toolCalls || []).some((call) => call.name === 'AskUserQuestion'); +}