baldart 5.8.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: simplify
3
3
  effort: medium
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  description: Review changed code for reuse, quality, and efficiency, then fix any issues found. Use when completing a coding task, after implementation, or when the user says /simplify. Covers deduplication (exact/near/inline), code quality patterns, and performance.
6
6
  ---
7
7
 
@@ -39,15 +39,31 @@ If the diff is empty (everything is committed), run `git diff HEAD~5..HEAD` to c
39
39
 
40
40
  If there are no git changes at all, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
41
41
 
42
- ## Step 2: Launch Three Review Agents in Parallel
42
+ > **Methodology SSOT**: the taxonomy (A1–A9), the canonical refactoring vocabulary (Fowler smells / Four Rules of Simple Design / AHA·YAGNI·Rule of Three), the clone-type vocabulary, the deterministic-tier contract, the internal-documentation awareness rules, and the judge-bias guards this skill applies all live in `framework/agents/simplify-protocol.md`. This body owns the OPERATIONAL workflow (Steps 1–5); it CITES the module for those rules rather than restating them (`SECTION=<taxonomy|canonical-vocab|clone-vocab|rubric|deterministic-tier|doc-awareness|bias-guards>`).
43
43
 
44
- Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
44
+ ## Step 1.5: Run the Deterministic Clone Floor (before eyeballing)
45
45
 
46
- > **Author-time prevention mirror:** `coder` now applies an author-time subset of the Quality + Efficiency taxonomy below WHILE writing (`framework/.claude/agents/coder.md § Author-Time Simplicity Discipline`), so the diff this pass reviews should already be cleaner on those domains. This pass remains the **independent net** its load-bearing value is the cross-codebase **Reuse** detection (exact/near duplicate, inline reimplementation, dependency-reimplements-native) that the author cannot see from inside the task. This Step 2 taxonomy is the SSOT; if you change it, keep the coder mirror in sync.
46
+ An LLM eyeballing a diff misses clones exactly as it misses its own smells so ground the Reuse lens with an EXTERNAL signal first (`agents/simplify-protocol.md SECTION=deterministic-tier`):
47
+
48
+ ```
49
+ node framework/.claude/skills/simplify/scripts/simplify-scan.mjs --diff HEAD --json
50
+ ```
51
+
52
+ (Zero-dep, identical on Codex; `--diff HEAD~5..HEAD` when the work is committed, or pass explicit file paths.) The scan PROPOSES anchored clone candidates (`path:line` × matching loci + type); it never decides. Pass its candidate list to Agent 1 below — Agent 1 verifies each (discards false positives like import blocks / boilerplate / fixtures, promotes real ones). A clean floor is a legitimate result, not a reason to invent duplicates.
53
+
54
+ ## Step 2: Launch the Code Simplifier
55
+
56
+ Use the Agent tool to launch **one `code-simplifier` agent** (`subagent_type: code-simplifier`), passing it the diff path + the Step 1.5 clone-floor candidates. It covers ALL the lenses below as one persona (Reuse / Quality / Design-altitude / Efficiency) — it is the independent, evidence-grounded reviewer; you (the skill) apply its fixes in Step 4. The lens descriptions below are its operational rubric.
57
+
58
+ > **Methodology SSOT**: the taxonomy, canonical vocabulary, clone-vocab, bias guards and doc-awareness the agent applies live in `framework/agents/simplify-protocol.md` — the lens descriptions here are the operational surface, not the SSOT. If you change a lens, keep the module + the `coder` author-time mirror (`framework/.claude/agents/coder.md § Author-Time Simplicity Discipline`) in sync.
59
+
60
+ > **Author-time prevention mirror:** `coder` already applies an author-time subset of the Quality + Efficiency + Design-altitude taxonomy WHILE writing, so the diff this pass reviews should already be cleaner there. This pass remains the **independent net** — its load-bearing value is the cross-codebase **Reuse** detection (exact/near duplicate, inline reimplementation, dependency-reimplements-native) the author cannot see from inside the task.
47
61
 
48
62
  ### Agent 1: Duplication & Reuse Review
49
63
 
50
- For each new piece of code in the diff:
64
+ **Consume the Step 1.5 clone-floor candidates FIRST** (pass them into this agent's briefing): for each anchored candidate, verify the match is a real duplicate/reuse-miss (not an import block, boilerplate, or fixture the scan over-matched) and confirm the "existing" side is LIVE code, not dead — then promote it to a finding with an `existing` pointer. The floor catches Type-1 and gapped Type-3; YOU still catch Type-2 (renamed identifiers, which the floor deliberately does not normalize) and Type-4 (semantic) via retrieval below. Classify every duplicate per `agents/simplify-protocol.md SECTION=clone-vocab`.
65
+
66
+ Then, for each new piece of code in the diff:
51
67
 
52
68
  1. **Start with `${paths.references_dir}/component-registry.md`** — the authoritative inventory of all UI primitives, shared components, hooks, and utility modules. Scan the relevant tables before grepping. Then search for existing utilities and helpers that could replace newly written code in the project's utility roots (e.g. `${paths.components_root}/shared/`, `${paths.components_primitives}/`, and `src/lib/`, `src/hooks/` if they exist), and files adjacent to the changed ones.
53
69
  2. **For each new component**, Grep the codebase for components with similar names, similar prop signatures, or similar JSX structure. Check for copy-pasted layout components (`layout.tsx` files with near-identical structure).
@@ -82,11 +98,15 @@ Review the same changes for hacky patterns:
82
98
  5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
83
99
  6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior
84
100
  7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)
101
+ 8. **Wrong-altitude abstraction (A4 — advisory)**: an interface/factory/generic/config-flag/parameter with exactly ONE caller or ONE implementation, or a speculative extension point with no present consumer — the Rule of Three is violated (do not abstract before the third occurrence). Name the smell (`Speculative Generality` / `Rule-of-Three`). Advisory, not a blocker — a *simplify* pass that fabricates abstraction complaints causes the very churn it exists to prevent (`agents/simplify-protocol.md SECTION=canonical-vocab`). Corollary caution (AHA): prefer a little duplication over unifying two blocks whose shared shape is only coincidental.
102
+ 9. **Pattern inconsistency vs siblings (A6 — advisory)**: the new code uses a different idiom / error-handling / naming / state pattern than the 2–3 nearest existing files it lives beside — align it to the established local convention.
103
+
104
+ > **Bias guard (a simplify judge is inverted)**: NEVER prefer a change because it adds structure — longer code is a NEGATIVE signal for this pass, never positive. Every finding cites executed/retrieved evidence (a clone match, a zero-caller proof, an existing-utility `path:line`), never unaided opinion. Zero findings on a clean diff is the correct outcome. Full rules: `agents/simplify-protocol.md SECTION=bias-guards`.
85
105
 
86
106
  #### Agent 2 — Design System SSOT Compliance sub-checklist (MANDATORY for UI refactors)
87
107
 
88
- This is part of Agent 2's single task (NOT a separate fourth parallel agent the three agents in
89
- Step 2 are Agents 1, 2, 3). When the diff includes files under `${paths.components_root}/`,
108
+ This is part of the Quality lens (Agent 2), which the single `code-simplifier` agent covers alongside
109
+ the other lenses NOT a separate agent. When the diff includes files under `${paths.components_root}/`,
90
110
  `${paths.components_root}/**/*.tsx`, or any `.css`/`.module.css`, the simplify pass MUST NOT introduce
91
111
  hardcoded design values while refactoring. Agent 2 runs this as part of its quality review:
92
112
 
@@ -116,7 +136,7 @@ Review the same changes for efficiency:
116
136
 
117
137
  ## Step 3: Aggregate Findings
118
138
 
119
- Collect results from all three agents. For each finding, categorize as:
139
+ Collect the `code-simplifier` findings (across all lenses). For each finding, categorize as:
120
140
 
121
141
  1. **Exact duplicate** — same logic exists elsewhere. Use the existing one. Delete the new one.
122
142
  2. **Near duplicate** — similar logic with minor differences. Extract a shared version with parameters.
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * simplify-scan.mjs — deterministic clone floor for the /simplify pass.
4
+ *
5
+ * The always-on, zero-dependency, stack-agnostic near-duplicate detector that
6
+ * grounds the code-simplifier reviewer with an EXTERNAL signal it lacks by
7
+ * eyeballing (research: an LLM misses clones exactly as it misses its own
8
+ * smells). Twin of ui-design/scripts/craft-check.mjs — runs identically on
9
+ * Claude Code and Codex (Node >= 18, no deps).
10
+ *
11
+ * It PROPOSES anchored clone candidates (path:line x matching loci + type); the
12
+ * reviewer DISPOSES (verifies each, discards false positives, promotes real
13
+ * ones). It NEVER edits and NEVER decides. Contract + rationale:
14
+ * framework/agents/simplify-protocol.md SECTION=deterministic-tier.
15
+ *
16
+ * Scope: the CHANGED files' code windows matched against the whole repo
17
+ * (diff x repo) AND against each other. Catches Type-1 (exact modulo
18
+ * whitespace/comments) and gapped Type-3; Type-2 (renamed identifiers) is
19
+ * deliberately NOT normalized away (it explodes boilerplate false positives —
20
+ * that is the reviewer's job), and Type-4 is structurally invisible here.
21
+ *
22
+ * Usage:
23
+ * node simplify-scan.mjs [--diff <ref-or-range>] [file ...] [--json]
24
+ * [--min-lines N] [--repo DIR]
25
+ *
26
+ * --diff <range> git range whose changed files are the "new" side
27
+ * (default: HEAD -> `git diff --name-only HEAD`; if that is
28
+ * empty, `HEAD~5..HEAD`). Positional files override detection.
29
+ * --min-lines N minimum window of non-trivial normalized lines (default 6).
30
+ * --json machine-readable output for the agent.
31
+ * --repo DIR repo root (default: `git rev-parse --show-toplevel` or cwd).
32
+ *
33
+ * Exit codes: 0 = ran (candidates, if any, are in the output — this is an
34
+ * advisory FEEDER, not a CI gate, so it never fails on findings), 2 = usage/env
35
+ * error. It is safe to run on any stack; a non-git or empty scope is a clean
36
+ * no-op, never an abort.
37
+ */
38
+
39
+ import { readFileSync, statSync } from 'node:fs';
40
+ import { execFileSync } from 'node:child_process';
41
+ import { join, extname, relative, isAbsolute } from 'node:path';
42
+
43
+ const argv = process.argv.slice(2);
44
+ const asJson = argv.includes('--json');
45
+ const getOpt = (name, def) => {
46
+ const i = argv.indexOf(name);
47
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : def;
48
+ };
49
+ const MIN_LINES = Math.max(3, parseInt(getOpt('--min-lines', '6'), 10) || 6);
50
+ const DIFF_REF = getOpt('--diff', null);
51
+ const positional = argv.filter(
52
+ (a, i) => !a.startsWith('--') && argv[i - 1] !== '--diff' && argv[i - 1] !== '--min-lines' && argv[i - 1] !== '--repo'
53
+ );
54
+
55
+ // ---- repo root ---------------------------------------------------------------
56
+ let REPO;
57
+ function git(args, opts = {}) {
58
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts });
59
+ }
60
+ try {
61
+ REPO = getOpt('--repo', git(['rev-parse', '--show-toplevel']).trim());
62
+ } catch {
63
+ REPO = getOpt('--repo', process.cwd());
64
+ }
65
+
66
+ // ---- what counts as a source file we index ----------------------------------
67
+ const SOURCE_EXT = new Set([
68
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte',
69
+ '.py', '.rb', '.go', '.rs', '.java', '.kt', '.swift', '.scala', '.php',
70
+ '.c', '.h', '.cc', '.cpp', '.hpp', '.cs', '.m', '.lua', '.dart', '.sh',
71
+ ]);
72
+ const IGNORE_DIR = /(^|\/)(node_modules|\.git|dist|build|out|\.next|coverage|vendor|__snapshots__|migrations|\.venv|venv|target|__pycache__)(\/|$)/;
73
+ const IGNORE_FILE = /(\.min\.[a-z]+$|\.map$|\.snap$|-lock\.[a-z]+$|package-lock\.json$|yarn\.lock$|pnpm-lock\.yaml$|\.d\.ts$)/i;
74
+ const MAX_BYTES = 512 * 1024; // skip huge/generated blobs
75
+
76
+ function isSource(rel) {
77
+ if (IGNORE_DIR.test(rel) || IGNORE_FILE.test(rel)) return false;
78
+ return SOURCE_EXT.has(extname(rel));
79
+ }
80
+
81
+ // ---- normalization: strip comments + whitespace, drop trivial lines ----------
82
+ // NO identifier normalization (deliberate — see header). A "trivial" line is
83
+ // pure punctuation, an import, a bare export brace, or a single keyword: the
84
+ // biggest false-positive sources for a naive hash.
85
+ const TRIVIAL_RE = /^(?:[{}()\[\];,]*|import\b.*|export\s*\{?\s*|export\s+default\s*|module\.exports.*|use\s+strict;?|package\s+\w.*|from\b.*|['"]use client['"];?|else\s*\{?|try\s*\{?|finally\s*\{?|return;?|break;?|continue;?|pass|end)$/;
86
+
87
+ function normalizeLines(text) {
88
+ // strip block comments spanning the file, then per-line line-comments.
89
+ const noBlock = text.replace(/\/\*[\s\S]*?\*\//g, '');
90
+ const out = [];
91
+ const rawLines = noBlock.split(/\r?\n/);
92
+ for (let i = 0; i < rawLines.length; i++) {
93
+ let s = rawLines[i];
94
+ s = s.replace(/(^|\s)(\/\/|#|--)\s.*$/, '$1'); // trailing line comment (space-guarded so it does not eat urls/operators mid-token)
95
+ s = s.replace(/\s+/g, ' ').trim();
96
+ if (!s) continue;
97
+ if (TRIVIAL_RE.test(s)) continue;
98
+ out.push({ norm: s, line: i + 1 }); // 1-based original line
99
+ }
100
+ return out;
101
+ }
102
+
103
+ // ---- window index ------------------------------------------------------------
104
+ // key = MIN_LINES consecutive normalized lines joined. Value = list of loci.
105
+ function fnv1a(str) {
106
+ let h = 0x811c9dc5;
107
+ for (let i = 0; i < str.length; i++) {
108
+ h ^= str.charCodeAt(i);
109
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
110
+ }
111
+ return h.toString(36);
112
+ }
113
+
114
+ function windowsOf(nlines) {
115
+ const w = [];
116
+ for (let i = 0; i + MIN_LINES <= nlines.length; i++) {
117
+ const slice = nlines.slice(i, i + MIN_LINES);
118
+ // contiguity guard: the window must not span an enormous original gap
119
+ if (slice[slice.length - 1].line - slice[0].line > MIN_LINES * 4) continue;
120
+ const key = fnv1a(slice.map((x) => x.norm).join('\n'));
121
+ w.push({ key, start: slice[0].line, end: slice[slice.length - 1].line });
122
+ }
123
+ return w;
124
+ }
125
+
126
+ function listTrackedSource() {
127
+ try {
128
+ return git(['ls-files'], { cwd: REPO }).split('\n').filter((f) => f && isSource(f));
129
+ } catch {
130
+ return [];
131
+ }
132
+ }
133
+
134
+ function changedFiles() {
135
+ if (positional.length) {
136
+ return positional.map((p) => (isAbsolute(p) ? relative(REPO, p) : p)).filter(isSource);
137
+ }
138
+ const tryRange = (r) => {
139
+ try { return git(['diff', '--name-only', r], { cwd: REPO }).split('\n').filter(Boolean); }
140
+ catch { return []; }
141
+ };
142
+ let files = tryRange(DIFF_REF || 'HEAD');
143
+ if (!files.length && !DIFF_REF) files = tryRange('HEAD~5..HEAD');
144
+ return files.filter(isSource);
145
+ }
146
+
147
+ function readSafe(rel) {
148
+ try {
149
+ const abs = join(REPO, rel);
150
+ if (statSync(abs).size > MAX_BYTES) return null;
151
+ const buf = readFileSync(abs);
152
+ if (buf.includes(0)) return null; // binary
153
+ return buf.toString('utf8');
154
+ } catch { return null; }
155
+ }
156
+
157
+ // ---- run --------------------------------------------------------------------
158
+ const changed = changedFiles();
159
+ if (!changed.length) {
160
+ const msg = { candidates: [], scanned: 0, note: 'no changed source files in scope — clean no-op' };
161
+ console.log(asJson ? JSON.stringify(msg, null, 2) : 'simplify-scan: no changed source files in scope (clean no-op).');
162
+ process.exit(0);
163
+ }
164
+
165
+ // index the whole repo (so a new block matching OLD code is caught)
166
+ const index = new Map(); // key -> [{file,start,end}]
167
+ const repoFiles = new Set([...listTrackedSource(), ...changed]);
168
+ for (const f of repoFiles) {
169
+ const txt = readSafe(f);
170
+ if (!txt) continue;
171
+ for (const w of windowsOf(normalizeLines(txt))) {
172
+ if (!index.has(w.key)) index.set(w.key, []);
173
+ index.get(w.key).push({ file: f, start: w.start, end: w.end });
174
+ }
175
+ }
176
+
177
+ // for each changed file window, report matches at OTHER loci
178
+ const changedSet = new Set(changed);
179
+ const seen = new Set();
180
+ const candidates = [];
181
+ for (const f of changed) {
182
+ const txt = readSafe(f);
183
+ if (!txt) continue;
184
+ for (const w of windowsOf(normalizeLines(txt))) {
185
+ const loci = index.get(w.key) || [];
186
+ const others = loci.filter((l) => !(l.file === f && l.start === w.start));
187
+ if (!others.length) continue;
188
+ const dedup = `${w.key}:${f}:${w.start}`;
189
+ if (seen.has(dedup)) continue;
190
+ seen.add(dedup);
191
+ const sameFile = others.every((l) => l.file === f);
192
+ candidates.push({
193
+ new: { file: f, start: w.start, end: w.end },
194
+ matches: others.map((l) => ({ file: l.file, start: l.start, end: l.end })),
195
+ clone_type: sameFile ? 'type-1 (intra-file)' : 'type-1/3 (cross-file)',
196
+ note: changedSet.has(others[0].file)
197
+ ? 'both sides are in the diff — likely copy-paste in this change'
198
+ : 'matches existing repo code — likely a reuse-miss (verify the existing side is live, not dead)',
199
+ });
200
+ }
201
+ }
202
+
203
+ // merge adjacent/overlapping candidate windows in the same new file into one
204
+ // region candidate (a sliding window over one duplicated block emits N
205
+ // overlapping hits — collapse them so the reviewer sees one finding per block).
206
+ candidates.sort((a, b) => (a.new.file < b.new.file ? -1 : a.new.file > b.new.file ? 1 : a.new.start - b.new.start));
207
+ const merged = [];
208
+ for (const c of candidates) {
209
+ const prev = merged[merged.length - 1];
210
+ if (prev && prev.new.file === c.new.file && c.new.start <= prev.new.end + 2) {
211
+ prev.new.end = Math.max(prev.new.end, c.new.end);
212
+ const key = (m) => `${m.file}:${m.start}`;
213
+ const have = new Set(prev.matches.map(key));
214
+ for (const m of c.matches) if (!have.has(key(m))) { prev.matches.push(m); have.add(key(m)); }
215
+ } else {
216
+ merged.push({ new: { ...c.new }, matches: [...c.matches], clone_type: c.clone_type, note: c.note });
217
+ }
218
+ }
219
+
220
+ if (asJson) {
221
+ console.log(JSON.stringify({ candidates: merged, scanned: repoFiles.size, changed: changed.length, min_lines: MIN_LINES }, null, 2));
222
+ } else {
223
+ if (!merged.length) {
224
+ console.log(`simplify-scan: 0 clone candidates across ${changed.length} changed file(s). Clean floor.`);
225
+ } else {
226
+ console.log(`simplify-scan: ${merged.length} clone candidate(s) (min ${MIN_LINES} lines) — VERIFY each (advisory, feeds the reviewer):\n`);
227
+ for (const c of merged) {
228
+ console.log(` ${c.new.file}:${c.new.start}-${c.new.end} [${c.clone_type}]`);
229
+ for (const m of c.matches) console.log(` ↔ ${m.file}:${m.start}-${m.end}`);
230
+ console.log(` ${c.note}\n`);
231
+ }
232
+ }
233
+ }
234
+ process.exit(0);
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * simplify-scan.test.mjs — fixture test for the clone floor.
4
+ *
5
+ * Proves the two properties that make the floor a signal and not noise:
6
+ * (1) SILENT on a boilerplate-heavy file (imports, component shell, braces).
7
+ * (2) CATCHES a real cross-file duplicated block.
8
+ *
9
+ * Zero-dep, runnable directly (`node simplify-scan.test.mjs`) or via
10
+ * `node --test`. Uses positional-file mode so no git repo is needed.
11
+ */
12
+
13
+ import { test } from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import { execFileSync } from 'node:child_process';
16
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join, dirname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'simplify-scan.mjs');
22
+
23
+ function runScan(dir, files) {
24
+ const out = execFileSync('node', [SCRIPT, '--repo', dir, '--json', ...files], { encoding: 'utf8' });
25
+ return JSON.parse(out);
26
+ }
27
+
28
+ const BOILERPLATE = `import React from 'react';
29
+ import { useState, useEffect } from 'react';
30
+ import { Button } from '@/ui/button';
31
+ import { Card } from '@/ui/card';
32
+
33
+ export default function Widget() {
34
+ const [open, setOpen] = useState(false);
35
+ useEffect(() => {
36
+ setOpen(true);
37
+ }, []);
38
+ return (
39
+ <Card>
40
+ <Button onClick={() => setOpen(!open)}>Toggle</Button>
41
+ </Card>
42
+ );
43
+ }
44
+ `;
45
+
46
+ // a genuine, non-trivial ~8-line block duplicated verbatim across two files
47
+ const DUP_BLOCK = `function computeInvoiceTotal(items, taxRate, discount) {
48
+ let subtotal = 0;
49
+ for (const it of items) {
50
+ subtotal += it.unitPrice * it.quantity;
51
+ }
52
+ const taxed = subtotal * (1 + taxRate);
53
+ const afterDiscount = taxed - taxed * discount;
54
+ const rounded = Math.round(afterDiscount * 100) / 100;
55
+ return rounded;
56
+ }
57
+ `;
58
+
59
+ test('silent on boilerplate-heavy file (no false positives)', () => {
60
+ const dir = mkdtempSync(join(tmpdir(), 'simpscan-'));
61
+ try {
62
+ const f = join(dir, 'widget.tsx');
63
+ writeFileSync(f, BOILERPLATE);
64
+ const res = runScan(dir, [f]);
65
+ assert.equal(res.candidates.length, 0, `expected 0 candidates, got ${res.candidates.length}`);
66
+ } finally {
67
+ rmSync(dir, { recursive: true, force: true });
68
+ }
69
+ });
70
+
71
+ test('catches a real cross-file duplicated block', () => {
72
+ const dir = mkdtempSync(join(tmpdir(), 'simpscan-'));
73
+ try {
74
+ const a = join(dir, 'billing.ts');
75
+ const b = join(dir, 'checkout.ts');
76
+ writeFileSync(a, `export const CFG = 1;\n${DUP_BLOCK}`);
77
+ writeFileSync(b, `export const OTHER = 2;\n${DUP_BLOCK}`);
78
+ const res = runScan(dir, [a, b]);
79
+ assert.ok(res.candidates.length >= 1, 'expected >=1 clone candidate for the duplicated block');
80
+ const hit = res.candidates.some(
81
+ (c) => c.matches.some((m) => m.file.endsWith('billing.ts') || m.file.endsWith('checkout.ts'))
82
+ );
83
+ assert.ok(hit, 'candidate should link billing.ts <-> checkout.ts');
84
+ } finally {
85
+ rmSync(dir, { recursive: true, force: true });
86
+ }
87
+ });
@@ -385,9 +385,11 @@ const qaPrompt =
385
385
  : '')
386
386
 
387
387
  function simplifyPrompt(c) {
388
- return `Simplify analysis (read-only — you do NOT edit, the workflow applies fixes afterward) over ONE card's committed diff, per ${protocolRef} (Phase 2.55). Cover all THREE lenses and return findings:\n` +
389
- ` • Reusenewly written code that duplicates an existing util/helper; inline logic that could use existing code; a NEW third-party dependency that reimplements a capability the language/stdlib/runtime/platform already provides (prefer the native primitive).\n` +
388
+ return `Simplify analysis (read-only — you do NOT edit, the workflow applies fixes afterward) over ONE card's committed diff, per ${protocolRef} (Phase 2.55) and your code-simplifier system prompt (agents/simplify-protocol.md). FIRST run the deterministic clone floor and verify its candidates, then cover ALL lenses and return findings:\n` +
389
+ ` • Clone floor run \`node "$(ls .claude/skills/simplify/scripts/simplify-scan.mjs .framework/framework/.claude/skills/simplify/scripts/simplify-scan.mjs 2>/dev/null | head -1)" --diff /tmp/diff-${c.cardId}.txt --json\` (missing skip, note 'scan-unavailable'); verify each anchored candidate (discard boilerplate/import/fixture false positives, confirm the existing side is LIVE) before promoting it.\n` +
390
+ ` • Reuse — newly written code that duplicates an existing util/helper (ground reuse-misses in the registry/code-graph/LSP per SECTION=doc-awareness, retrieve-then-verify); inline logic that could use existing code; a NEW third-party dependency that reimplements a stdlib/platform capability (prefer the native primitive).\n` +
390
391
  ` • Quality — redundant state, parameter sprawl, copy-paste with slight variation, leaky abstractions, stringly-typed code where enums exist, unnecessary JSX nesting, WHAT-comments / narration.\n` +
392
+ ` • Design-altitude (advisory) — single-caller/one-implementation abstraction, Rule-of-Three violation, speculative generality with no consumer, pattern inconsistency vs the 2-3 nearest sibling files. Do NOT fabricate abstraction complaints (churn is the anti-goal).\n` +
391
393
  ` • Efficiency — redundant computation, duplicate API calls, N+1, missed concurrency, hot-path bloat, missing change-detection guards, unbounded structures.\n\n` +
392
394
  `${cardScopeBrief(c)}\n\n${baselineBrief}\n\n` +
393
395
  `Run a false-positive check on every finding and SUPPRESS the unconvincing ones (your surviving findings are treated as validated). ` +
@@ -405,7 +407,7 @@ function securityPrompt(c) {
405
407
  const findThunks = []
406
408
  for (const c of cards) {
407
409
  if (c.runSimplify !== false) {
408
- findThunks.push(() => reviewSafe('simplify', c, simplifyPrompt(c), { label: `simplify:${c.cardId}`, phase: 'Discovery', schema: FINDINGS_SCHEMA }))
410
+ findThunks.push(() => reviewSafe('simplify', c, simplifyPrompt(c), { label: `simplify:${c.cardId}`, phase: 'Discovery', agentType: 'code-simplifier', schema: FINDINGS_SCHEMA }))
409
411
  }
410
412
  if (c.hasSecurityFiles === true) {
411
413
  findThunks.push(() => reviewSafe('security', c, securityPrompt(c), { label: `security:${c.cardId}`, phase: 'Discovery', agentType: 'security-reviewer', schema: FINDINGS_SCHEMA }))
@@ -37,7 +37,8 @@ Route agents to the right module with minimal reading.
37
37
  - If touching terminology or naming -> read `agents/coding-standards.md`.
38
38
  - If touching env vars or scripts -> read `agents/runbook.md` and `agents/env-reference.md`.
39
39
  - If touching operational procedures (backup, cleanup, secrets) -> read `docs/operations/` if it exists in your project.
40
- - If touching security or auth risk -> read `agents/security.md`.
40
+ - If touching security or auth risk -> read `agents/security.md` (OWASP 2021 posture + secure-defaults). If performing a dedicated AppSec review (the `security-reviewer` agent, `/codexreview` security pass) -> also read `agents/security-review-protocol.md` (the matching `SECTION=` only): boundary-gate → repo-scoped threat-model → high-miss discovery-lens → class proof-tuples → structured attack-path → adversarial-refute (attacker≠victim) → coverage-ledger. It EXTENDS `review-protocol.md` (generic passes), never duplicates it.
41
+ - If reviewing changed code for reuse/duplication/simplicity (the `code-simplifier` agent, the `/simplify` skill, `/new` Phase 2.55) -> read `agents/simplify-protocol.md` (the matching `SECTION=` only): deterministic clone floor → doc-awareness retrieval → taxonomy (A1–A9) + canonical-vocab (Fowler / Four Rules / AHA·YAGNI·Rule-of-Three) + clone-vocab → rubric lenses → bias-guards (a simplify judge never rewards verbosity). It EXTENDS `review-protocol.md` (generic passes), never duplicates it; partitioned from `code-reviewer` (correctness/security stay there).
41
42
  - If touching performance limits or scaling -> read `agents/performance.md`.
42
43
  - If touching monitoring/logging -> read `agents/observability.md`.
43
44
  - If tuning reasoning depth — setting a skill's `effort:` baseline or honoring an inline `effort=<level>` override -> read `agents/effort-protocol.md`.
@@ -86,6 +87,8 @@ When adding or updating agents, update REGISTRY.md — not this file.
86
87
  - `agents/runtime-portability-protocol.md` — Runtime-mechanics binding: the abstract-operation ↔ Claude/Codex map (spawn / permissions / workflow-accel / state-spine / decision-gate / read-write path / adversarial-vs-cross-model), detect-once capability contract; cited by `/new` + `/prd`. The runtime-mechanics twin of `effort-protocol.md` + `return-contract-protocol.md` (since the Codex-parity S4 wave)
87
88
  - `agents/agent-operating-protocol.md` — Shared operating procedures for the core agents (injection guard, doc-retrieval consumption, persistent-memory hygiene, tool-budget discipline), `SECTION=` dispatch, read only the matching section; agents keep 1-line binding versions inline (since v5.0.0)
88
89
  - `agents/review-protocol.md` — The shared verification engine for reviewers: Challenge + Actionability, Simulation (diff-walk / plan-walk), Chain-of-Verification, quantified risk scoring + absolute severity calibration, specialist-spawn discipline incl. orchestrated-mode `specialist_dispatch` suppression; `SECTION=` dispatch (since v5.0.0)
90
+ - `agents/security-review-protocol.md` — The AppSec verification engine for `security-reviewer`: boundary-gate, repository-scoped threat-model, modern high-miss discovery-lens (fail-open / allowlist-escape / control-regression / gate-action mismatch / parser differential / CI-CD trust / IaC omitted arg / over-broad grant), class-specific proof-tuples (method-anchored confidence, instance-preserving), structured attack-path → mechanical severity, adversarial-refute (attacker≠victim, precondition vs counterevidence), coverage-ledger; `SECTION=` dispatch. EXTENDS `review-protocol.md`, never duplicates it. Portable Claude+Codex (since v5.9.0)
91
+ - `agents/simplify-protocol.md` — The reuse & simplicity verification engine for `code-simplifier` (+ the `/simplify` skill + the `coder` author-time mirror): agentic-defect taxonomy (A1–A9 with detectable signals), canonical refactoring vocabulary (Fowler smells / Four Rules of Simple Design / AHA·YAGNI·Rule-of-Three / Beck tidyings), clone-type vocabulary (Type-1/2/3/4), the deterministic clone-floor contract (`simplify-scan.mjs`), internal-documentation awareness (retrieve-then-verify reuse retrieval), and the judge-bias guards a simplify reviewer needs (no verbosity reward, evidence-over-opinion, independence); `SECTION=` dispatch. EXTENDS `review-protocol.md`, never duplicates it. Portable Claude+Codex (since v5.10.0)
89
92
  - `agents/doc-audit-protocol.md` — doc-reviewer's audit-mode procedures (drift validator suite, topological generation, epistemic metadata, SCIP anchors, schema/registry drift, coverage gauges); read ONLY when invoked without card context; `SECTION=` dispatch (since v5.0.0)
90
93
  - `agents/research-protocol.md` — Research discipline: `PROFILE=<decision|deep|compare|regulatory>` output contracts (+ `DEPTH=` iterative loop), the research library (`paths.research_dir` — layout, report frontmatter, INDEX, archive-not-delete), the reuse pre-flight (`FULL_REUSE`/`DELTA`/`NEW` + per-category TTLs), and the versioned source matrix with its growth loop (`SOURCE_MATRIX_CANDIDATE`); `SECTION=` dispatch. The research-side sibling of `analysis-profiles.md` (since v5.1.0)
91
94