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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## What this does

<!--
2–3 plain sentences a teammate who didn't write the code can skim: what changed
and why it matters — the problem it solves or the behaviour it changes. Name the
real-world effect ("login now survives a refresh"), not the mechanism
("refactored the auth persistence layer").

Signal, not spam: every sentence earns its place; explain the *why*, not just the
*what*; don't make a small change sound big; no jargon dump, no restating the
diff, no AI filler. If you can't say it plainly, the change is unclear — that's a
signal, not a cue for bigger words.
-->

## Summary

<!-- Technical bullets — the *how*, for reviewers reading the code. -->

-

## Test Plan

- [ ]
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ All notable changes to cc-settings are documented here.

## [Unreleased]

## [11.16.0] — 2026-06-02

Two additions: a **deslop advisory probe** in the proof-of-work gate (the framework-agnostic sibling to react-doctor), and a shift to **PR-by-default** as the standard workflow (with a repo PR template that dogfoods the plain-English standard).

### Added

- **deslop advisory probe** — `src/lib/proof-of-work.ts` gains `detectDeslop()`, `runDeslop()`, and the pure, unit-tested `sumDeslopFindings()`; `bun run proof` appends a deslop pass when the project depends on `deslop-cli`. Same shape as the react-doctor probe: **advisory** (never flips the verdict or exit code), and **opt-in by dependency** so local `npx` resolves the pinned binary with no network fetch. deslop (millionco, MIT) is a framework-agnostic cross-file dead-code / unused-export / circular-import scanner — the deterministic floor under the `deslopper` agent, catching what Biome's per-file linting can't. Reports total findings across categories (`X findings`). Silent for projects without it.
- **`config/30-permissions.json`** — narrow `Bash(npx deslop:*)` allow rule (not a blanket `npx`). `docs/settings-reference.md` permissions block regenerated.
- **`rules/typescript.md`** — Tools bullet documenting the pinned deslop invocation as an advisory pre-filter.
- **`.github/PULL_REQUEST_TEMPLATE.md`** — a repo PR template embodying the v11.15.0 plain-English standard ("What this does" → Summary → Test Plan).

### Changed

- **PR-by-default workflow** — `rules/git.md` adds an "Open a PR by default" note: feature-branch + PR is the norm (most Darkroom client projects protect `main`), and direct `git push origin main` is the reserved exception for repos that explicitly allow it. Corrects the prior assumption that direct-to-main was a standing default.
- **`skills/proof-of-work/SKILL.md`** — the advisory-probe paragraph now covers both react-doctor and deslop (was react-doctor only).

## [11.15.2] — 2026-06-02

Fix a pre-existing **Windows** bug in the `/freeze` edit-scope lock. It was latent on `main` since `/freeze` shipped (`ee74a4e`) because changes had been direct-pushed without watching the Windows CI jobs — and it surfaced the moment a PR's CI matrix was actually watched to completion (a payoff of moving to PR-by-default).
Expand Down
1 change: 1 addition & 0 deletions config/30-permissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Bash(biome:*)",
"Bash(lighthouse:*)",
"Bash(npx react-doctor:*)",
"Bash(npx deslop:*)",
"Bash(open -a:*)",
"Bash(ls:*)",
"Bash(pwd:*)",
Expand Down
1 change: 1 addition & 0 deletions docs/settings-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ Bash(gh:*)
Bash(biome:*)
Bash(lighthouse:*)
Bash(npx react-doctor:*)
Bash(npx deslop:*)
Bash(open -a:*)
Bash(ls:*)
Bash(pwd:*)
Expand Down
6 changes: 6 additions & 0 deletions rules/git.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ Technical bullets — the *how*, for reviewers reading the code.
- No jargon dump, no restating the diff, no filler ("This PR introduces a comprehensive…").
- If you can't say what it does in plain English, that's a signal the change is unclear — not a reason to reach for bigger words.

### Open a PR by default
Default to a feature branch + PR, even for small changes — most Darkroom client
projects protect `main` and review through PRs. Direct `git push origin main` is
the exception, reserved for repos that explicitly allow it and changes that are
trivial and self-evident. When unsure, open a PR.

### Before Merging
- CI passes, code reviewed, no conflicts, branch up to date

Expand Down
1 change: 1 addition & 0 deletions rules/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ function get<T>(obj: T, key: keyof T) { return obj[key] }
## Tools
- **Biome** - Linting and formatting
- **tsc --noEmit** - Type checking
- **deslop** *(optional — when `deslop-cli` is in the project's deps)* — framework-agnostic cross-file dead-code / unused-export / circular-import scanner that catches what Biome's per-file linting can't. Run pinned: `npx deslop` (`--json` for a machine-readable report, `--fail-on-issues` to gate CI). `bun run proof` auto-includes it as an advisory signal when present.
2 changes: 1 addition & 1 deletion skills/proof-of-work/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ It detects `typecheck` / `test` / `lint` from `package.json`, runs them cheapest
- exit 0 → `review-ready ✓`
- exit 1 → `NOT review-ready ✗` — fix the failing gate before a human looks

For React projects that depend on `react-doctor`, the gate also runs it as an **advisory** probe (the project's pinned binary, telemetry off): the score is reported but never flips the verdict — a deterministic signal alongside the hard gates, not a blocker. It stays silent for projects that don't depend on it.
Projects can opt into **advisory** probes by depending on the tool — the gate then runs the project's pinned binary: **react-doctor** (React render/quality score, telemetry off) and/or **deslop** (framework-agnostic cross-file dead-code count). Advisory results are reported but never flip the verdict — deterministic signals alongside the hard gates, not blockers. Silent for projects that don't depend on them.

For UI changes, attach a screenshot (`/qa` or the chrome-devtools MCP) as the visual half of the proof — tests can't prove "looks right".

Expand Down
43 changes: 42 additions & 1 deletion src/lib/proof-of-work.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
export type GateName = "typecheck" | "test" | "lint";

// Advisory probes run alongside the hard gates but never flip the verdict.
export type ProbeName = GateName | "react-doctor";
export type ProbeName = GateName | "react-doctor" | "deslop";

export interface GateResult {
gate: ProbeName;
Expand Down Expand Up @@ -46,6 +46,15 @@ export function detectReactDoctor(deps: Record<string, string>): boolean {
return "react-doctor" in deps;
}

/** deslop is an OPTIONAL advisory probe — a framework-agnostic cross-file
* dead-code / unused-export scanner (millionco `deslop-cli`) that catches what
* Biome's per-file linting can't. Same opt-in rule as react-doctor: runs only
* when the project depends on `deslop-cli`, so local `npx` resolves the pinned
* binary with no network fetch. Pass the merged deps + devDeps map. */
export function detectDeslop(deps: Record<string, string>): boolean {
return "deslop-cli" in deps;
}

/** Overall verdict: green iff no NON-ADVISORY gate FAILED. Skips are allowed (a
* project without a lint script isn't "not review-ready"), and advisory probes
* never block regardless of their status. */
Expand Down Expand Up @@ -103,3 +112,35 @@ export async function runReactDoctor(cwd: string): Promise<GateResult> {
}
return { gate: "react-doctor", status: "pass", detail: `score ${score}/100`, advisory: true };
}

/** Run the deslop advisory probe. Local `npx` resolves the project's pinned
* binary; `--json` returns a flat object of finding-category arrays plus scalar
* metadata (`totalFiles`, `analysisTimeMs`, …). Total findings = the sum of the
* array-valued fields, so the scalars are naturally excluded. Always advisory:
* any non-zero exit or unparseable output is a SKIP, never a fail. */
export async function runDeslop(cwd: string): Promise<GateResult> {
const proc = Bun.spawn(["npx", "deslop", "--json"], {
cwd,
stdout: "pipe",
stderr: "ignore",
});
const [out, exit] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
const total = sumDeslopFindings(out);
if (exit !== 0 || total === null) {
return { gate: "deslop", status: "skip", detail: "unavailable", advisory: true };
}
return { gate: "deslop", status: "pass", detail: `${total} findings`, advisory: true };
}

/** Sum the finding-category arrays in a deslop `--json` report. Returns null if
* the text isn't a parseable JSON object (pure, so it's unit-testable without a
* subprocess). */
export function sumDeslopFindings(jsonText: string): number | null {
try {
const report = JSON.parse(jsonText) as Record<string, unknown>;
if (typeof report !== "object" || report === null) return null;
return Object.values(report).reduce<number>((n, v) => n + (Array.isArray(v) ? v.length : 0), 0);
} catch {
return null;
}
}
27 changes: 19 additions & 8 deletions src/scripts/proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
// scarce serial resource (human attention) on judgment, not on confirming what
// a machine could.
//
// For React projects that depend on react-doctor, an extra ADVISORY probe runs
// its deterministic scan. It reports a score but never flips the verdict.
// Projects can opt into extra ADVISORY probes by depending on the tool:
// react-doctor (React render/quality) and deslop (framework-agnostic cross-file
// dead code). They report a signal but never flip the verdict.

import {
allGreen,
detectDeslop,
detectGates,
detectReactDoctor,
formatReport,
runDeslop,
runGates,
runReactDoctor,
} from "../lib/proof-of-work.ts";
Expand All @@ -28,16 +31,24 @@ const pkg = (await Bun.file("package.json")
devDependencies?: Record<string, string>;
};
const gates = detectGates(pkg.scripts ?? {});
const hasReactDoctor = detectReactDoctor({ ...pkg.dependencies, ...pkg.devDependencies });
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const hasReactDoctor = detectReactDoctor(deps);
const hasDeslop = detectDeslop(deps);

if (gates.length === 0 && !hasReactDoctor) {
const advisoryLabels = [
...(hasReactDoctor ? ["react-doctor (advisory)"] : []),
...(hasDeslop ? ["deslop (advisory)"] : []),
];

if (gates.length === 0 && advisoryLabels.length === 0) {
console.log("Proof of work: no verify scripts (typecheck/test/lint) in package.json — skipping.");
process.exit(0);
}

const labels = [...gates, ...(hasReactDoctor ? ["react-doctor (advisory)"] : [])];
console.log(`Running proof-of-work gates: ${labels.join(", ")} …\n`);
const results = await runGates(gates, process.cwd());
if (hasReactDoctor) results.push(await runReactDoctor(process.cwd()));
console.log(`Running proof-of-work gates: ${[...gates, ...advisoryLabels].join(", ")} …\n`);
const cwd = process.cwd();
const results = await runGates(gates, cwd);
if (hasReactDoctor) results.push(await runReactDoctor(cwd));
if (hasDeslop) results.push(await runDeslop(cwd));
console.log(formatReport(results));
process.exit(allGreen(results) ? 0 : 1);
2 changes: 1 addition & 1 deletion src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import type { StatusData } from "./lib/status-types.ts";
import { buildVersionDelta, readInstalledVersion } from "./lib/version-delta.ts";
import { Settings } from "./schemas/settings.ts";

const VERSION = "11.15.2"; // freeze: cross-platform boundary check (path.sep, not hardcoded /) — fixes Windows
const VERSION = "11.16.0"; // deslop advisory probe in proof-of-work; PR-by-default workflow + PULL_REQUEST_TEMPLATE
const CLAUDE_DIR = join(homedir(), ".claude");

// --- Arg parsing ---------------------------------------------------------
Expand Down
31 changes: 31 additions & 0 deletions tests/proof-of-work.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import { describe, expect, test } from "bun:test";
import {
allGreen,
detectDeslop,
detectGates,
detectReactDoctor,
formatReport,
sumDeslopFindings,
} from "../src/lib/proof-of-work.ts";

describe("proof-of-work lib", () => {
Expand Down Expand Up @@ -63,4 +65,33 @@ describe("proof-of-work lib", () => {
expect(report).toContain("ℹ react-doctor — score 87/100 (advisory)");
expect(report).toContain("review-ready ✓");
});

test("detectDeslop: true only when deslop-cli is a dependency", () => {
expect(detectDeslop({ "deslop-cli": "0.0.14", typescript: "6" })).toBe(true);
// deslop-js (the lib) is not the bin package — only deslop-cli counts
expect(detectDeslop({ "deslop-js": "0.0.14" })).toBe(false);
expect(detectDeslop({})).toBe(false);
});

test("sumDeslopFindings: sums finding arrays, ignores scalar metadata", () => {
const report = JSON.stringify({
unusedFiles: [{ path: "a" }, { path: "b" }],
unusedExports: [{ name: "x" }],
circularDependencies: [],
totalFiles: 120,
analysisTimeMs: 42,
});
expect(sumDeslopFindings(report)).toBe(3);
expect(sumDeslopFindings("{}")).toBe(0);
expect(sumDeslopFindings("not json")).toBeNull();
});

test("deslop advisory: rendered with (advisory), never blocks the verdict", () => {
const results = [
{ gate: "typecheck" as const, status: "pass" as const },
{ gate: "deslop" as const, status: "pass" as const, detail: "12 findings", advisory: true },
];
expect(allGreen(results)).toBe(true);
expect(formatReport(results)).toContain("ℹ deslop — 12 findings (advisory)");
});
});
Loading