npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@addforce/governor

v0.4.17

Published

Local, deterministic MCP server that scores a coding task against your repo — likely files, difficulty, criticality, model recommendation, verification level — plus an optional Claude Code hook channel. No LLM calls, no network, WASM-only parsing.

Readme

Governor

Governor is a local MCP server for a coding agent (or the human driving one): before a task starts, it says which files it'll touch and which model to use for it; after, it catches an agent's "done" claim that doesn't actually hold up.

  • Local engine, no network. File-inference, difficulty, and criticality scoring run as pure rules-based logic (packages/core) — no LLM call, no network request, no API key required.
  • Stops and asks instead of guessing. When a task description is too vague to place confidently, it asks a clarifying question rather than silently picking files.
  • Catches a "done" claim that didn't hold up. verify_task detects and runs the project's own test/build/lint commands and diffs files actually changed against what was predicted, so a task claimed done with failing tests or an untouched prediction doesn't slip through.

Docs in /docs; this file is the quickstart. Shipped-version history: CHANGELOG.md.

Install (npm)

Language coverage: full support, import-graph included, for TypeScript/JavaScript/Python. Every other real-source-code file gets text-tier support — indexed, embedded, and keyword-matched, without the dependency-graph signal a parser would add. (Full breakdown below the install commands.)

Registering the MCP server alone does not make it active. The server only runs when a tool is called by name — nothing calls assess_task on its own. Install the hook below to get a brief injected automatically on every prompt instead of relying on an agent to remember to ask.

The product packs as two artifacts: @addforce/governor (the MCP server, the governor bin, and the Claude Code hook channel with its governor-hook installer bin, all in one package) and @addforce/governor-core (the engine it depends on, WASM grammars and the vendored embedding model included — nothing downloads at install or run time). Install:

npm i -g @addforce/governor                # pulls @addforce/governor-core automatically
governor-hook /path/to/your/project    # registers BOTH the MCP server and the hook — one command
governor-hook /path/to/your/project --uninstall    # removes both any time

That's the whole install — two commands. governor-hook writes to THREE places, every one of them named in the confirmation prompt before anything happens: it merges a "governor" entry into <project>/.mcp.json (creating the file if it doesn't exist, leaving any other server entries there untouched), merges UserPromptSubmit/Stop entries into <project>/.claude/settings.json, and appends a .governor/ line to <project>/.gitignore (creating it if absent) so the record store never shows up as untracked files in your repo. Every one of the three is named again in the success message once the install finishes, and in the refusal if it can't ask you. --uninstall reverses the first two exactly — a file it CREATED is removed entirely, along with the .claude/ directory if creating that file is the only reason it exists; a file that already existed keeps its other content minus our entries — but does not touch the .gitignore line, which is left in place as a harmless, permanent convenience (the uninstall message says so rather than leaving you to find it). If .mcp.json already has a "governor" entry pointing somewhere else, the installer refuses and says so rather than overwriting it — remove or rename that entry first.

governor-hook always asks for confirmation unless you pass --yes — this holds whether or not you gave it an explicit target directory; earlier versions skipped the prompt for an explicit path, which is not what "asks for confirmation first" should mean, and no longer does. The install itself also prints this same command right after it finishes (with --foreground-scripts, since npm hides plain postinstall output by default — see below) — see it there if you skip straight to the code. governor-hook works on any real project directory, git or not (it refuses only a directory that looks empty or unrelated — no manifest file and no source code found, one that already has the hook installed, or a conflicting .mcp.json entry as above; --help for the full option list). Without it, the server still works once you register it yourself (below) — an agent can call assess_task explicitly — it just won't happen on its own, and you're back to hand-editing .mcp.json.

Running governor with no arguments prints getting-started instructions rather than hanging as an MCP server nobody is talking to. governor-hook with no arguments does the same when you are not inside a project directory; run it from inside one and it treats that as the target and asks to install, which is the other thing you might have meant. Either way you do not need to remember the two commands above from memory.

governor is the stdio server itself — an MCP client spawns it, you don't run it directly. If you'd rather register it by hand (a non-Claude-Code client, or you don't want the hook), or want to see what the installer writes: Claude Code's .mcp.json at your project root, or claude mcp add; other clients use their own MCP server config, same shape:

{
	"mcpServers": {
		"governor": {
			"command": "governor",
			"args": []
		}
	}
}

or, without the -g flag (a local install into one project instead):

{
	"mcpServers": {
		"governor": {
			"command": "node_modules/.bin/governor",
			"args": []
		}
	}
}

(and node_modules/.bin/governor-hook for the hook command above, in that case)

Both packages are licensed under PolyForm Shield 1.0.0 (see Licence below) and publish under the @addforce scope. pnpm gate4 packs both tarballs, installs them with plain npm into a fresh directory outside this repo, and drives a full MCP handshake, cold index, and hook install/uninstall round-trip against a never-seen clone with the network blocked (gate-harness/reports/gate4-packaging-2026-08-13.md).

Language coverage, in full: full support, import-graph included, for TypeScript/JavaScript/ Python; every other real-source-code file (Dart, Elixir, Lua, Zig, Haskell, R, Julia, Clojure, F#, C#, Java, Go, Rust, C/C++, Ruby, PHP, Kotlin, Swift, Scala, Objective-C, shell, SQL, Vue, Svelte, template languages like EJS/Jade/Handlebars, and anything else that isn't positively identified as data/markup/config, documentation, an image/media/binary, or generated/minified output) gets text-tier support — indexed, embedded, and keyword-matched, just without the dependency-graph signal a parser would add. This is a denylist, not a hardcoded list of languages: a language nobody thought to name still gets indexed, rather than silently contributing nothing.

Check it's working

After registering the server (above), confirm two things: your MCP client sees it, and a real call comes back with a real answer.

The client sees it connected. In Claude Code, run /mcp — governor should show as connected, and its tool list should include assess_task, assess_plan, get_file_risk, report_outcome, verify_task, reindex. Other clients have their own way to list connected MCP servers and tools; the tool names are the same everywhere.

A real call comes back with a real answer. Ask your agent to call assess_task with a short description of something you'd actually work on next in this repo (in Claude Code, just describe a task normally — it calls the tool on its own once registered). A working response looks like this shape, always:

<one plain-language summary line — the recommended model and why, or a clarifying question>

**Record ID:** `<timestamp>-<random>`

_Closing the loop: call `report_outcome` with this record id once the task is done...

If the repository you registered against isn't a git repo yet (or has no commits), the response starts with one extra line — NOTE: no git history available here... — and everything else still works; that's expected, not a problem to fix.

If you get anything else — an error message, a stack trace, or no response at all — something is actually wrong: check that the command in your .mcp.json resolves at all (command -v governor, or command -v node_modules/.bin/governor for a local install, from the same shell your client uses — it's a stdio server, so running it directly just hangs waiting for input, which is itself a sign it resolved correctly), and see Development below for pnpm gate4, which reproduces this exact check from a clean install outside the monorepo and is the fastest way to tell "my setup" from "a real bug" apart.

Using Governor outside Claude Code

Governor indexes the directory it's launched from — its cwd — unless told otherwise. Claude Code launches every MCP server it registers from the project directory itself, so this needs no attention there. Other clients don't all do that: some launch a global MCP server from wherever the IDE's own install lives, which is a different directory from any project you're actually working in. Point Governor at the right one with the GOVERNOR_REPO environment variable — it overrides cwd when set, and takes precedence over it.

{
	"mcpServers": {
		"governor": {
			"command": "npx",
			"args": ["-y", "@addforce/governor"],
			"env": {
				"GOVERNOR_REPO": "/absolute/path/to/your/project"
			}
		}
	}
}

GOVERNOR_REPO must resolve to a real, existing directory that looks like an actual project (a package.json/pyproject.toml/.git/etc. at its root, or a source file within two directory levels — the same check the installer already uses, so if governor-hook would accept a directory as a target, this accepts it as GOVERNOR_REPO too). Point it at the wrong path, or at a directory that isn't a real project, and every repo-reading tool (assess_task, assess_plan, reindex, get_file_risk, verify_task) refuses outright and says exactly why — naming the resolved path — instead of silently falling back to cwd and indexing whatever happens to be there. That refusal never writes anything: no .governor/ directory, no files, until it's pointed at somewhere real.

Which models Governor will recommend

Governor only names a model it believes your environment can actually run. It used to score the same four-model Claude set everywhere, which meant that inside a non-Claude client it would happily recommend claude-opus-5 — a model that client cannot run. Availability now resolves from three layers, each overriding the one before:

  1. CLI scan. Which vendor agent CLIs (claude, gemini, codex, …) are on your PATH. This is the most direct evidence there is, because every model call Governor makes runs through your own CLI. Scanned once and cached in .governor/cli-scan.json; run reindex after installing a new CLI to refresh it.
  2. Host identity. The clientInfo your MCP client sends during the handshake, matched against a short table of known clients. A client not in that table resolves to unknown — never a guess.
  3. Your own declaration. availableModels via manage_settings, which overrides both:
manage_settings action=set key=availableModels value="claude-sonnet-5, gemini-3.7-flash"

When Governor can't name a model, it says so instead of guessing. If nothing in its scored catalog is available in your environment, the response names the tier the task needs and stops there — no model id. This is deliberate: an agent handles missing information far better than wrong information, so a tier phrase beats a confident recommendation you can't act on.

Making other models recommendable by name. Governor's scored catalog is the four Claude tiers. It also ships a 36-model research catalog (RESEARCH_CATALOG) with real prices and benchmark numbers for models from Google, OpenAI, xAI, DeepSeek and others — but deliberately without tiers or task-type fit, because assigning those is a judgment call this project won't make on your behalf. To have a non-Claude model recommended by name, add it to .governor/registry.json with a tier you choose:

[
	{
		"id": "gemini-3.7-flash",
		"displayName": "Gemini 3.7 Flash",
		"tier": "balanced",
		"inputCostPerMTok": 0.3,
		"outputCostPerMTok": 2.5,
		"seedScores": {},
		"seedConfidence": "seed"
	}
]

That entry is scored like any other from then on. The tier is your declaration about your own setup, which is exactly the boundary: Governor won't invent one, and it won't stop you from setting one.

Quickstart

pnpm install
pnpm build

Register the server (already checked in at the repo root as .mcp.json — most MCP clients, including Claude Code and VS Code, pick this up automatically once you cd into the repo):

{
	"mcpServers": {
		"governor": {
			"command": "node",
			"args": ["packages/adapter-mcp/dist/index.js"]
		}
	}
}

First call is slower than the rest. The first assess_task/assess_plan/reindex call against a repo builds the full index — file scan, git history, dependency graph, criticality scores — and warms the semantic embedding model (all in-process, no download at request time; the model ships vendored in the package). Expect this to take several seconds on a mid-sized repo. Every call after that reuses the cached index and only rebuilds when HEAD moves. Call reindex explicitly to force a full rebuild and see the cost broken out:

Reindexed `/path/to/repo`.

- Files: 214
- Commits: 812
- HEAD: `a1b2c3d4e5`
- Semantic: 214 files, 1,043 chunks, 0 cache hits / 1,043 misses, 15,821ms index time
- Watcher: 1 records attributed, 5 memory entries created, 42 commits processed
- Total: 16,234.1ms

Reduced vs. full mode

The phase-gate docs (docs/phase-gates.md) describe a future "full mode" — a real usage-measurement gate that could eventually justify a richer, LLM-in-the-loop enrichment layer for file inference. That layer doesn't exist yet: as shipped, every response comes out of the deterministic reduced-mode engine, whether or not you have an API key configured anywhere else in your toolchain. There is no mode switch to flip today — "reduced mode" isn't a fallback, it's the whole product right now. Every registry entry is correspondingly marked seedConfidence: "seed", and file-inference confidence never claims more certainty than the rules-based signals actually support.

What leaves your machine

Not one blanket claim — the layers differ in what they actually send, so each gets its own line:

  • The engine (packages/core: ranking, difficulty, criticality, model recommendation) makes no LLM call and no network request, unconditionally. This is the claim above, and it does not weaken as other layers are added on top of it.
  • The intake layer's CALLER path (packages/adapter-mcp/src/tools.ts's assessTask, on the MCP channel or any live turn-taking caller) introduces no new privacy surface: it asks the CALLING AGENT — which is already inside a session the description already went to — to compute a clarity score, a specification-vocabulary rewrite, and a model recommendation itself, and pass the answer back on the next call. Nothing new leaves the machine that the session's own model calls did not already receive. On by default for exactly that reason.
  • The intake layer's SPAWN path (packages/adapter-hooks/src/intake-spawn.ts) would be a genuinely NEW outbound call the user did not otherwise initiate — a headless claude CLI invocation carrying the task description — if it were enabled. It is DISABLED. The real implementation is preserved and tested (unsafeSpawnIntakeModelCaller), but the path everything actually calls (createSpawnIntakeModelCaller) always rejects immediately without spawning anything, because the call measures ~10s against the hook channel's own 800ms IPC budget — 12x over, and awaiting it inline would reopen the exact fail-open regression gate-harness/reports/hook-fail-open-2026-08-18.md fixed. .governor/hooks-config.json's intakeSpawnEnabled flag still exists and is still read, but has no effect. As a result, the hook channel makes zero outbound model calls today, full stop — see docs/intake-layer-spec.md's "Why the SPAWN path is disabled" section for the exact condition under which that would change (a genuine non-blocking dispatch, or a materially faster spawn).
  • "auto" automation mode's dispatch (packages/adapter-mcp/src/auto-dispatch.ts) is a genuinely NEW outbound call too — the router runs the task itself, spawning a headless claude CLI call carrying the (rewritten) task description. It ships off by default: .governor/intake-config.json's automation is "recommend" unless explicitly set to "auto". Unlike the SPAWN path above, this one has no budget conflict (it only ever runs from a live MCP/no-channel caller, never the hook channel), so it isn't disabled — but it is exactly as opt-in. See docs/intake-layer-spec.md's "The automation switch" section.
  • The done-verification layer (packages/core/src/verify/, verify_task) makes NO outbound call of any kind — no model call, no network request. It only runs commands the project ALREADY defines (its own test/build/lint scripts) via a local subprocess spawn (packages/adapter-mcp/src/command-runner.ts), on your own machine, and never modifies the repo, commits, or installs anything.

Don't take our word for it

The automated test: scripts/gate4-pack-test.mjs (run via pnpm gate4) launches the packed server with poisoned proxy env vars and samples its live TCP connections every 5s while it handles a real assess_task call — see sampleOutbound() in that file.

Watch it yourself: start the server, find its PID, then watch its connections while you send it a request — Get-NetTCPConnection -OwningProcess <PID> (PowerShell) or lsof -i -p <PID> / netstat -p tcp <PID> (macOS/Linux). You should see nothing but loopback/local, if anything at all.

The only path that ever reaches the network at runtime: "auto" automation mode's dispatch (packages/adapter-mcp/src/auto-dispatch.ts), which spawns a real claude CLI call. It is opt-in and off by default — see What leaves your machine above.

The tools

Registered by packages/adapter-mcp/src/index.ts. Every assess_task/assess_plan/reindex response carries structured.serverBuild: {startedAt, buildHash, stale} — a cheap mtime check against the server's own compiled entry and @addforce/governor-core's, so a rebuild-without-restart surfaces immediately instead of silently serving stale formulas; when stale is true the markdown opens with a one-line restart reminder.

assess_task(description, files?)

The main entry point. Scores every registry model against the task, infers which files it likely touches (unless you already know and pass files), and — when confidence is high enough — renders an opening brief: a ready-to-paste starting-file list with the signal that justified each entry, capped extended-checklist candidates below it, and the verification level required on completion.

## Task Assessment

**Recommended model:** claude-opus-5

| Model | Score | Est. cost | Breakdown |
|---|---|---|---|
| **claude-opus-5** | 0.85 | $0.3149 | taskTypeFit 0.86, difficultyFit 1.00, costEfficiency 0.56 |
| claude-sonnet-5 | 0.77 | $0.1889 | taskTypeFit 0.85, difficultyFit 0.67, costEfficiency 0.78 |

**Verification:** affected-tests — touches a medium-criticality file.

**Record ID:** `1786617849385-7a337663`

```
OPENING BRIEF (from Governor, confidence: medium)
Likely starting files — verify before editing, do not limit yourself to these:
- packages/adapter-mcp/src/tools.ts — name match, semantic, co-change
- packages/core/src/measurement/records.ts — name match, semantic, co-change, prompt memory
Extended checklist — lower-ranked candidates worth a quick look before broad exploration:
- gate-harness/src/report.ts — name match, semantic, co-change, prompt memory
Verification required on completion: affected-tests — touches a medium-criticality file.
If these files look wrong, say so and explore normally.
```

Below confidence "medium with basename evidence," a tentative brief renders instead (hedged wording, 3-file cap) whenever the top candidate still scores >= 0.6; below that, nothing renders and a clarifying question takes its place. A fourth tier, docs-hedged, replaces whatever would have rendered when the description explicitly names a real, indexed non-code file (a README, a config file) — near-certain evidence the task can't be seen by a ranker that only ranks code — OR when the task merely classifies as docs/config by keyword AND no code-file candidate clears the tentative floor either. A confident code-file candidate is trusted over a casual docs-sounding description: sampling every unit whose text merely read as documentation work found 15 of 15 touched real source in the actual commit (gate-harness/reports/task-kind-map-2026-08-17.md), so keyword classification alone no longer overrides a ranker that has a real lead. structured.briefTier (full / tentative / docs-hedged / none) and structured.timingMs ({total, contextReady, inference, semantic, decide}) are on every response for whoever wants to measure this without scraping markdown — see Honest numbers.

Caller-rewrite: recovering recall on unconfident descriptions

Recall falls as a task description drifts from commit-message vocabulary toward casual or vague phrasing (gate-harness/reports/ai-ground-baseline-2026-08-17.md). Rewriting the description into commit-message register — before scoring it — measurably recovers most of that gap (gate-harness/reports/query-rewrite-2026-08-17.md: casual R@5 51.5% → 64%+ after rewriting; a follow-up round confirmed the rewrite itself does the work, not an incidental directory listing the first measurement's prompt also carried — gate-harness/reports/query-rewrite-followup-2026-08-18.md).

Two ways to get that rewrite; this project ships the cheaper one as the default.

Caller-rewrite (default on this MCP channel). When a description scores an unconfident combination of signals, assess_task's response gains a callerRewriteHint: an instruction asking you, the calling agent, to rewrite the description into commit-message vocabulary and call assess_task again — no subprocess, no separate auth, one extra turn in the session you're already running. Validated with a REAL separate caller (a fresh headless session per unit, not the same session doing the validating) on the two repos where recall actually collapses: 30 units, 15 each of scrapy and nest, casual + vague text only — see gate-harness/reports/caller-rewrite-validation-2026-08-18.md for the per-repo numbers against the spawned-subprocess alternative below.

The hint doesn't fire on every unconfident call — that was measured and rejected. Gating on confidence !== "high" alone fires on effectively every call ("high" never occurs on the holdout used to measure this), so a real gate was calibrated instead, entirely on the four tuning repos (zod/fastify/flask/express) — never on the holdout data used to validate it: fires when at least 3 of 4 structural signals are weak (a low top score, a small gap to the second candidate, a terse description, no basename evidence on the top candidate). Read once against already-scored holdout data (no new generation, no threshold changes after looking): calls the gate fires on gained 19.9% mean R@5 on average; calls it suppressed gained 7.0% — it fires more on the descriptions that actually benefit, and it correctly never fires on the one repo where rewriting doesn't move the needle at all (gate-harness/reports/caller-rewrite-gate-2026-08-18.md).

Default is channel-dependent: on for this (MCP) channel and any caller with no channel at all, off on the Claude Code hook channel (its injection is a one-shot prompt prepend with no multi-turn "read the hint, rewrite, call again" loop to run in). Either default can be overridden per call with the callerRewrite boolean.

Spawned subprocess (measured, not shipped as a live feature). The alternative — the server spawns a headless claude -p process to do the rewrite itself — was measured, not built into the product: a real call costs ~40,757 tokens (median, dominated by the spawned CLI's own system-prompt/tool-definition injection — this is real per-call weight a fresh process always pays, regardless of the actual prompt) and ~14s wall-clock when run in a real project directory (~10.6s in a bare one), with no known quota price — haiku has no empirical entry in scripts/quota-weights.json yet. Caller-rewrite's own real cost, measured live in the session that validated it: a single extra turn adds roughly 1,100–2,100 tokens of genuinely NEW content (cache-write + output) — but the FULL cost of any turn in a long-running session also re-reads its accumulated context (cache-read pricing, ~10x cheaper per token than fresh input, but non-zero): in an ~800K-token session that re-read alone outweighs the spawned call's fixed cost, while in a short, fresh session caller-rewrite is unambiguously far cheaper. Which one is actually cheaper depends on how much context the calling session already carries — there is no single answer, which is exactly why the default routes through the caller (cheap in the common case, and the only path with zero fixed per-call token floor) while the spawn path stays available as a measured, documented alternative rather than a shipped one.

If the response contains a Questions for the USER section, that's the clarifying-question path: the calling agent is expected to relay it to the human verbatim and stop, not answer it internally (the control contract lives in CLAUDE.md).

assess_plan(steps[])

Same scoring, one step at a time, for a planning agent that already knows which files each step of a multi-step plan touches:

## Plan Assessment

| Step | Recommended model | Difficulty | Band | Verification |
|---|---|---|---|---|
| 1 | claude-sonnet-5 | 0.42 | medium | affected-tests |
| 2 | claude-opus-5 | 0.71 | high | full-suite |

reindex()

Forces a full rebuild regardless of whether HEAD has moved — see the Quickstart output above. Normally unnecessary; every other tool call already rebuilds on a cheap HEAD mismatch.

Two smaller tools round out the API: get_file_risk(path) looks up one file's criticality score and components without running a full assessment, and report_outcome(recordId, chosenModelId?, note?) manually records which model was actually used for a prior decision (automated attribution from git history runs separately and does not need this call).

verify_task(recordId, claimedDone)

The done-verification layer (docs/intake-layer-spec.md's Step 4). Call it after finishing the task named by recordId, reporting whether your own final message claims it's done. Detects and runs the project's own test/build/typecheck/lint commands, diffs files actually changed against what was predicted, and checks any done conditions intake captured — then renders one line, in the same spirit as the intake message:

Verification: LOW — tests did not run, 1 of 3 predicted files touched

This is a confidence level, never a correctness verdict — it catches BROKEN work (tests fail, nothing runs, files diverge from the prediction), not WRONG-but-passing work (code that runs clean but doesn't do what was actually asked). Runs entirely locally: no model call, and it never modifies the repo, commits, or installs dependencies — see "What leaves your machine" above.

The clarifying-question dial

docs/intake-layer-spec.md's Step 5, shipped as MEASURABLE rather than proven — the case for asking rests on one directionally-positive small-sample measurement, not a settled result. .governor/intake-config.json's interventionLevel (0-100, default 20) asks a clarifying question on the N% least-clear of THIS project's own prompts, read as a percentile of the project's own clarity-score history (never an absolute score — scores aren't comparable across models). 0 never asks, 100 always asks. The question itself comes from the SAME intake call that already returns the score, rewrite, and recommendation — the model is required to return null rather than invent one when nothing is genuinely missing.

When the dial fires, assess_task returns early with just the question:

Clarifying question: <question>

**Record ID:** `1786617849385-7a337663`

Relay it to the user, then call assess_task again with the same description, intakeRecordId, and either intakeQuestionAnswer or intakeQuestionSkipped: true — capped at exactly one question round per task. In "auto" automation mode a question still stops for the user first: automation governs dispatch, not consent to be asked.

The Claude Code hook channel

The second channel over the same engine: instead of an agent calling a tool, a UserPromptSubmit hook asks a warm background daemon for the assessment and injects context directly into the prompt — before the session reads your prompt. A Stop hook closes the loop: it reads which model actually ran from the session transcript, stamps it onto the session's decision records, and triggers a watcher sweep so commits you just made attribute automatically.

pnpm build
pnpm install-hook     # merges hook entries into .claude/settings.json, verifies the daemon starts
pnpm uninstall-hook   # removes exactly our entries, stops the daemon

Injection policy: what actually gets injected, and for whom

What the hook prints is no longer the same for every session — it is gated by tier AND by an injection profile, on measured evidence rather than assumption:

  • Tier none injects nothing. It used to print a one-line "low confidence" notice; the variance-floor round's Part 0 measured that the existing brief already costs strong executors tokens for no completion benefit on this population, and a consolation line when the render gate itself withheld a brief is the same shape of cost for less reason to pay it.
  • Cheap executors (sonnet, haiku) get the full brief — file list, extended checklist, the advisory model line — unchanged from before this policy existed.
  • Strong executors (opus, fable) get one line only: Governor: task assessed (confidence <level>); full brief via assess_task — no file list, no checklist. The variance-floor round measured that the file-list brief does not stabilize (and slightly destabilizes) these two executors; the AI layer's prompt-rewrite round separately measured that a wrong specific guess is worse than an honest absence of one. A strong executor is pointed at assess_task for the full brief on demand rather than handed one it does not need and may act on unnecessarily.
  • Executor detection reads the last assistant turn's model from the session transcript (transcript_path, the same field the Stop hook already reads) — best-effort, not live: a session's first prompt has no prior turn to read, so it falls through to config. Set .governor/hooks-config.json's injectionProfile ("cheap" / "strong" / "auto", default "auto") to override detection when it has nothing to say, or to force a profile regardless of what's detected — detection always wins over config when it fires.
  • assess_task called explicitly always returns the full brief, on every channel, regardless of profile — a human (or agent) that asked the question directly gets the full answer; the profile only shapes what the hook chooses to push unprompted.
  • Every hook-channel record gains injectionProfile and injectedBytes — which profile actually rendered and how many bytes actually went into the session's context — so this policy's real effect is measurable from the records alone, not just assumed from the tier.

The safety rail is absolute: every hook path fails open. On daemon absence, any error, or a hard 800ms budget, the hook prints nothing and exits 0 — your prompt proceeds untouched, and the failure goes to .governor/hook.log, never to your face. The first prompt after a cold start usually gets no brief (the daemon is still warming); the next one does.

Warm latency scales with repository size, and not gently. Measured directly (warm assess_task, p50/p95 over 20 calls, real repo content, no synthetic fixtures):

| Repo | Tracked files | p50 | p95 | |---|---|---|---| | json-server | 33 | 18.5ms | 20.3ms | | flask | 236 | 66.7ms | 70.7ms | | this monorepo | 963 | ~3.0s | ~3.1s |

A 4x file-count jump (flask → this monorepo) costs a 45x latency jump, not the ~4x a linear relationship would predict — file-inference scoring is effectively all of it (2,994ms of the monorepo's 3,010ms total; 50ms of flask's 64ms), which is worth flagging as a real, unoptimized scaling issue rather than a rounding error. On a repository this monorepo's size, warm latency exceeds the hook channel's own 800ms hard budget — a prompt here will typically fail open (no brief, silent, by design) rather than deliver one. Below a few hundred tracked files, in practice, warm latency is a non-issue. The assess_task MCP tool has no hard timeout of its own and always returns the real answer, however long it takes; it is only the hook channel's fail-open budget that this can blow through on a large repo.

Measure delivery, not installs. A working install still fails open sometimes — a slow daemon under real load, a machine mid-something-else — and a failed-open call produces no DecisionRecord at all, so records.jsonl alone cannot tell you how often the hook actually delivered a brief. It only tells you how often it succeeded, silently dropping every failure from the denominator. Every hook invocation, delivered or failed-open, is logged with its outcome and latency to .governor/hook-delivery.jsonl (pnpm dashboard, monorepo-only — see above, reports the delivered share over the last 7 days) — any field evaluation of the hook channel should read from there, not from install counts or from records.jsonl alone. This was a real gap, not a hypothetical one: a controlled measurement (docs/dev-log.md, "greenfield: four arms under hidden tests") found the hook delivering nothing in up to 8 of 18 sessions of a real multi-arm run, invisible until the invocation itself was logged.

The honest limitation: a hook cannot change a running session's model — nothing it injects can flip the picker for you. The product recommends and asks; switching is the user's action. Row 141 made the recommendation itself actionable instead of purely informational, but did not (could not) close that gap: when the RUNNING model's tier differs from the RECOMMENDED model's tier, the injected line instructs the agent to stop before starting work and ask you whether to switch, naming both models and the reason — the same "relay verbatim and stop" shape this project's own CLAUDE.md already uses for clarifying questions. A same-tier difference (two models in the same tier) never triggers this — only a tier change does — and it fires at most once per session per tier-mismatch pairing, not on every prompt. Whether the agent actually obeys that instruction is a separate, measured question, not an assumption: .governor/tier-offers.jsonl records every offer and, once the transcript shows what the next turn did, whether it was obeyed — see docs/dev-log.md row 141 for the mechanism and its own "no obedience-rate claim without data" caveat. Recommendation-vs-reality lands in the records regardless (recommendedModelId from the assessment, modelActuallyUsed from the transcript), which is the comparison the product's story actually needs.

Development

pnpm build
pnpm test        # FAST tier: pure unit tests, ~8s total — run this constantly
pnpm test:slow   # SLOW tier: real daemon + real IPC latency-budget tests, ~94s
pnpm test:all    # both, in order
pnpm lint

Any change touching a package runs that package's SLOW tests before commit; pnpm test:all in full before any publish and at the end of a working session. pnpm test alone cannot catch a latency regression in the hook or MCP channel — the two tests that would are excluded from it by design. Full rationale, the per-file timing measurement behind the split, and why the daemon budget test specifically runs serially: docs/testing.md.

Honest numbers

Full validation detail: docs/current-state.md (current formulas, gate results, known limitations) and docs/experiment-log.md (every measured round, chronologically, with cited sources). Summarized plainly, no rounding in the flattering direction:

  • The opening brief's file list — what the calling agent actually receives — recalls roughly 64% of the right files on casually-worded English task descriptions, at the shipped confidence gating. Extending that same ranking down through the extended checklist to rank ~20 recovers recall to roughly 90% on the same population — most of the gap the top-of-list brief misses is still there, just further down, not gone (gate-harness/reports/recall-curve-2026-08-13.md).
  • Zero silent failures on every measured variant: the "high confidence" bucket has never once been wrong-and-certain across any validation round to date (docs/current-state.md §5).
  • The no-AI ceiling is formally closed. Four full measurement rounds after the current formula shipped — a corrected re-ranking hypothesis, three stronger-embedding-model swaps, and an exhaustive per-prompt error clinic sweeping 167 configurations — moved casual-English recall by at most +0.21pp against a pre-registered +3pp bar, or won on the tuning set and lost on the frozen holdout. Deterministic lexical/graph/co-change/embedding re-ranking has reached its ceiling on this problem; the verdict and full evidence chain are in docs/current-state.md §5's "Post-v19: the error clinic and the no-AI ceiling."
  • Gate 1's own recall/precision thresholds still fail outright against their original targets (recall 74.4% vs. >= 80%, precision 25.6% vs. >= 50%) — mitigated, not closed, by confidence gating and clarifying questions.

None of this is presented as a finished product. It's the actual state of the numbers, and the numbers are the pitch, not a summary written to sound better than they are.

This project measures itself

Every round of formula work runs against a frozen test set (real OSS commit history, sampled once and never re-touched during tuning) plus a calibration set the tuning is allowed to see, disjoint by construction — and, past that, an express holdout repo never opened for tuning at all, to catch a formula that wins on what it was fitted to and loses on what it wasn't. Pre-registered success/failure criteria are written down before a round runs, not after — see docs/experiments/opening-brief.md for the currently-running protocol (opening-brief token savings, BRIEF vs. CONTROL, alternating assignment).

docs/dev-log.md records every development task with the model that ran it, why that model, an API-equivalent token/cost estimate computed from local session transcripts (scripts/log-usage.mjs), and — where the routing engine's own recommendation diverged from what actually ran — that divergence, noted rather than smoothed over. The project is, deliberately, a pilot of itself.

📊 Cost dashboard — the project's own spend in both currencies (API-equivalent dollars and subscription-quota points), per-model breakdown, brief-tier distribution, timing percentiles. Static HTML, no dependencies, no network; regenerate with pnpm dashboard. Monorepo-only: it summarizes THIS repository's own docs/dev-log.md and .governor/ history — a project-self-measurement tool, not a feature the installed @addforce/governor package ships or runs against your repo. There is currently no equivalent command available from a plain npm i install.

Licence

You may use this software, including for commercial work, and modify it for your own use. You may not redistribute it, sell it, or use it to build a competing product — see LICENSE (PolyForm Shield 1.0.0) for the exact terms.