Decisions this project has already made about its own shape. An architecture review should not re-suggest what is recorded here; if a decision is wrong, write a new ADR that supersedes it rather than quietly undoing it.
The protocol-level decisions — evidence must be fetched not typed, the gate fails open and
loud, the two overrides, the machine-wide core.hooksPath — are recorded in
research/DISCOVERY.md under Already decided, and in
docs/superpowers/specs/2026-09-13-research-gate-hardening-design.md. Those predate this
directory and are treated as ADR-equivalent.
| ADR | Decision |
|---|---|
| 0001 | The canonical project shape has one owner (lib/scaffold.mjs), and a deployed hook's executable bit is part of its contract |
| 0002 | Machine-scoped state lives in lib/machine.mjs; the fail-open posture has one reader, plus one pinned copy in shell for the missing-kit case. Refined by 0020 on 2026-09-15: its "a broken config never blocks work" holds for a config that was never written and not for one that exists and cannot be parsed — that is the third state, which holds the last config that parsed and otherwise fails closed. ADR-0002 itself is left unedited, in its own words |
| 0003 | The research corpus has one reader and one writer (lib/corpus.mjs); malformation is reported, never absorbed. Its loadCorpus was removed 2026-10-17 (#297) - nothing called it; callers pair readCorpus with verifyLedger (the ADR is a bundled record and is not edited for this; only the kit's name changed in it, ADR-0189) |
| 0004 | The contract checks are an ordered registry (lib/checks.mjs); the verdict stays one function |
| 0005 | All vendor knowledge lives in one Firecrawl adapter (lib/firecrawl.mjs) with an injectable exec |
| 0006 | The Cowork binding is the project's own .claude/skills/ (install.mjs --into); the ~/.agents/skills mirror is gone |
| 0007 | docs/ARCHITECTURE.md is the map of the code, and it is staged in the same commit as the code it maps — enforced by the commit gate (architecture-map); a prompt, not a proof |
| 0008 | The map rule's guarded paths are project configuration (research/kit.json, defaults src/lib/bin/scripts/app); this repo declares its own, enforcement surfaces included |
| 0009 | The Finding extractor stays in the collector — a dated rejection of the 2026-09-14 review's candidate 5, with the two triggers that expire it (a second consumer of the extractor, or the heuristics outgrowing their file). Superseded by 0016 on 2026-09-15: trigger 2 fired (the extractor was 460 of the collector's 658 lines). ADR-0009 itself is left unedited, in its own words |
| 0010 | The kit runs on two machines: a machine declares role: collector | builder in machine config (default collector), a builder's missing Firecrawl credential is informational, and a builder is held to the rule that it must not collect — refused at the collection CLIs |
| 0011 | The handoff is checked where the corpus arrives: lib/handoff.mjs verifies ledger present, every cited capture on disk, chain intact — a blocker on a builder, silent on a collector (whose existing reporters own the same gaps), with one remedy (the collector pushes research/raw/ including dotfiles). Refined by 0020 on 2026-09-15: the remedy is now chosen from the cause, because a corpus that travelled whole and was rewritten on checkout here is fixed with .gitattributes on this machine — the single text sent operators to the collector, and in practice to re-collect, for evidence that was already on disk. ADR-0011 itself is left unedited, in its own words |
| 0012 | The kit names no runtime: capability names everywhere (editGate, --edit-only, hooks/edit-gate.mjs), the runtime's paths as ONE anchored, overridable default (RUNTIME_ANCHORS + runtimePaths/skillLocations), a rename that repairs a stale registration instead of doubling it, and dated records left in their own words |
| 0014 | The brief's shape has one owner (lib/brief.mjs: BRIEF_SECTIONS, which sections are judged, the section reader the audit uses), and "not yet drafted" is a structural marker in the scaffold rather than a sentence of template prose — briefState → template | legacy | draft | authored, with a pre-marker scaffold (legacy) refusing loudly instead of guessing |
| 0015 | The trace joins (evidence row → its fetch and capture; unknown → the claim it rests on) are lazy pure functions on the corpus reader (traceOf, captureOf, claimOf + capturesByFile), not precomputed row.trace data — the reader is the edit-time gate's read path and must not pay for joins nobody asked for; re-raises the 2026-09-14 review's deferred candidate 3, with a measurement as the expiry trigger Trigger measured 2026-09-26 and not fired (docs/measurements/2026-09-26-trace-and-collect-cost/): traceOf over every row costs 110 ms and a full preflight 532 ms at 2,000 pages - quadratic, and immaterial |
| 0016 | The Finding extractor moves to lib/finding.mjs (firstFinding(markdown, fallback) is its whole interface) — superseding ADR-0009, whose trigger 2 fired: the extractor was 70% of lib/collect.mjs, a second concept in a file that declares one. The collector imports it and does not re-export it; the 17 heuristic tests moved with it Noted 2026-09-26 (here, because ADR-0016 is part of the frozen 2026-09-17 bundle, ADR-0028): its "Trigger 1 has not [fired]" is no longer true - lib/artifact.mjs has called firstFinding since 589f3d5 (2026-09-21) to flag Finding cells never rewritten (ADR-0032). The split now answers ADR-0009's second-consumer trigger too; the decision stands, and its revisit condition is not met, because the interface did not have to grow |
| 0013 | Resuming after an interruption is documentation, not a check: an arriving agent orients from repository state (doctor, preflight, the last commit report, this map, the working tree) and resumes rather than restarts — prose in AGENTS.md/template, enforced by nothing, because a gate that judged orientation would be wrong |
| 0017 | The wrong project is asked for, not searched for: the project is the cwd and the kit takes no project argument (a flag would make two roots the gate can disagree about); a cwd that is not the project the operator meant is one question, never a filesystem search, an inference from a name, or a remote lookup — prose in AGENTS.md/template, enforced by nothing |
| 0018 | The operator's file (START_HERE.md) ships from template/ into every scaffolded project — the short human-readable twin of AGENTS.md, where things are / how to start / how to resume / credits — and is deliberately NOT a LAYOUT entry: no rule needs it to exist, so a project without it reports nothing |
| 0019 | Audits bundle into one attachment: zipAudit reads the manifest's latest pointers (never a directory listing), renders nothing and bumps nothing, and refuses rather than guesses (no audits / several unnamed topics / a manifest naming a missing file). The ZIP container is written in lib/archive.mjs — no dependency, no shelling out to a zip binary; an exact --topic beats prefix matching |
| 0020 | Five trusted boundaries that did not hold, fixed as one class: no data reaches a shell (argv + shell: false, the Windows .cmd route validated and refusing rather than re-quoted); the collector's lock is O_EXCL, so exclusivity is the kernel's decision, with an unidentifiable fresh holder waited for rather than broken; a path list is data on a pipe, never a growing argv, and a watchdog kill is a named internal error; platform text handling is pinned (.gitattributes in the repo and the template) and a body-hash mismatch is classified — line-ending rewrite vs. tampering — so the remedy names the right machine; and the machine config default is read in three states, where an unreadable config holds the last one that parsed and otherwise fails closed. Four shared rules: the guarantee belongs to the platform; a data channel is not an argument channel; classify rather than guess; a hardening never moves towards allowing on its own. Refines 0002 and 0011, both left unedited |
| 0021 | The test harness awaits its tests: runPending is async, a verdict is printed only after the test has settled, and the failure count includes async failures — before, an async test printed ok the instant it returned its promise and the runner exited 0 over a failing assertion. One test at a time in registration order (concurrency rejected: shared temp state); a per-test watchdog (FETCHPROOF_TEST_TIMEOUT, 60s, chosen by measurement — the suite runs in 9s); a timed-out test's promise swallowed so its late rejection cannot crash the runner. Discharges finding 1 of the 2026-09-15 triaged review and the deferral ADR-0020 recorded, both left unedited |
| 0022 | Phase 2 builds the protocol kit completely and defers the release-evidence validator layer (release-validator, path-authority-validator and the other three), naming what is delivered and covered by the suite rather than shipping five half-modules to satisfy a module table |
| 0023 | A role the kit cannot establish does not collect: unknown is a third role state, not a declarable value — machineRole() resolves to collector | builder | unknown while ROLES stays the two an operator may write, so nobody can configure their way into an ambiguity |
| 0024 | The commit gate judges the index, every other caller judges the working tree: materializeIndex reproduces the index's own bytes in a scratch directory via git ls-files -z + git checkout-index --prefix, never writing to the working tree, and the verdict carries which one it judged. Its rejected "materialise the whole index" has a trigger that fired: ADR-0183 runs the suite rule on a scratch repository built from the whole index |
| 0025 | One writer boundary around the whole durable operation — collectOne holds the lock for the cache decision, the adapter call, the capture write, the id allocation, the ledger append and both row updates — and liveness rather than age as the staleness test (kill(pid, 0)), because a synchronous collector blocks its own event loop and can never heartbeat |
| 0026 | A refresh triggers claim review and never substitutes silently: a twelfth check, evidence-supersession, refuses an unknown resting on a row whose URL has since been re-collected. The old capture is kept — evidence is irreplaceable — but a superseded row may not keep carrying an unknown |
| 0027 | Search and fetch are two seams, not one adapter: selectTransport resolves a fetch provider and a search provider independently, FETCH_SHAPE/SEARCH_SHAPE are declared, and the return value is extended ({ name, adapter, why } still means the fetch side) so no existing caller changes. Refines ADR-0005, which stays correct about where vendor knowledge lives. The point is two meters, not a lower unit price — a capture is stamped with what fetched it, and transport-provenance is deliberately untouched |
| 0028 | The bundle is a received state, not a live constraint: BUNDLE_INDEX.md's digests are frozen at the 2026-09-17 handover and never regenerated, because a hash file rewritten whenever it disagrees with reality records nothing. Supersedes ADR-0022's byte-preservation consequence and restores ADR-0007 in full — no more --no-verify for the map rule. The trade ADR-0022 described was inverted: nothing had ever read those digests, while the map rule is checked mechanically on every commit and caught four real documentation errors in three days. lib/bundle.mjs makes the record checkable instead of decorative — unchanged / changed-and-expected / changed-and-not-expected, with EXPECTED_TO_DRIFT naming only the files the project's own rules require to move. Superseded in part by ADR-0189: the 44 planning documents that spelled the kit's former name were renamed with it on 2026-10-22, and their rows rewritten once |
| 0029 | The validator layer arrives as a source, not a donor: the release-evidence modules ADR-0022 deferred turn out to exist on this machine - 1,700 lines, 12 schemas, 61 passing tests - in a tree that is not a git repository and has diverged from this one. Absorbing them in one commit would be the project failing its own premise, since it refuses an unaccounted capture and could not coherently exempt code. Six rules bind the port: one module per commit, tests green in THIS runner, every module read before it is committed, nothing enters ahead of its dependencies, the map moves in the same commit, and the provenance the source cannot supply is written here |
| 0030 | Research supporting a repository-level architectural decision lives in a self-contained project at docs/decisions/<date>-<decision-name>/ — scaffolded normally, verified by cd-ing into it, and committed with its ledger. A scratch project may be used for experiments, but its findings cannot support a committed decision unless the corpus and ledger are preserved. Written after the first attempt at one such decision collected nine pages into a temporary directory and deleted it as cleanup, leaving a recommendation with no provenance under it. Costs no code: the project has always been the cwd (ADR-0017) |
| 0031 | Actions-first delivery, and any client pins X-GitHub-Api-Version. Evidence basis: docs/decisions/2026-09-21-delivery-architecture/research/BRIEF.md. The dispatch endpoint returns 204 No Content under the default 2022-11-28 and 200 with {workflow_run_id, run_url, html_url} under 2026-03-10 — verified both ways against this repository, which removed the correlation race that was the main objection to the Actions route. The ZIP is labelled COLLECTED CORPUS — HUMAN REVIEW REQUIRED inside the package, is delivery and never storage (90 days), and the paid run stays behind a required reviewer. The local .exe is deferred on useVfs being Stability 1.0, not rejected |
| 0032 | One artifact contract for every consumer, and authorization is DERIVED rather than asserted. One versioned ZIP serves the AI agent, the Actions workflow, the future Windows app and a person with unzip: manifest.json is the machine entry point, README-FIRST.md the human one, and the producer renders the second from the first so they cannot disagree. createArtifact takes no authorization argument and the CLI has no --build-authorized — state, kind, buildAuthorized, review.* and gate.* all come from running the real gate, with env defaulting to {} so a packaging shell's GATE_OFF cannot leak into a package that outlives it. findingsReviewed is answered negatively rather than graded: a Finding still byte-identical to the extractor's output was demonstrably never rewritten (the line ADR-0013 drew, uncrossed). The validator is offline and read-only, and refuses every structural fault from the central directory before inflating anything. PASS and buildAuthorized stay separate fields, because exit 0 on a collected corpus is the most misreadable result the tool produces |
| 0033 | The collector refuses rather than degrades, and no dispatch input reaches a shell. collect.yml implements ADR-0031 against ADR-0032's contract, separate from live-collection.yml because "does the adapter work" and "collect this, for a person, now" deserve different approvers. The environment is the spend gate — and is checked for existence, because GitHub silently creates an unprotected environment when a workflow names one that is missing (E-06), which would remove the approval gate without failing anything. The honest limit is written in the file: GitHub exposes no scope information, so the marker proves the environment was configured and cannot prove a repository-level value did not serve the secret. No http-keyless fallback: a missing credential refuses rather than shipping a quietly worse corpus under the same artifact name. Every input arrives through env: and then an argv array — ${{ inputs.x }} in a run: block is substitution before the shell parses, so a backtick in a topic executes. The test enforcing that scans every workflow and found live-collection.yml had been doing it since it was written; fixed here Amended the same day: the required reviewer is dropped and the environment kept. A reviewer approving a dispatch is a spend gate, not a review — nothing is collected yet to judge — and it makes autonomous dispatch impossible. The environment stays because it was never about approval: it is the only place the credential is unreadable by every other workflow. Spend is bounded by max_pages, the depth tier and the vendor's 402-at-zero cap. The control that replaces the reviewer is token scope: a fine-grained PAT with Actions: read and write on one repository, driven by bin/collect-remote.mjs, which takes no --token flag and redacts by pattern rather than by remembering its own value |
| 0034 | The collector speaks MCP over stdio (its one-chain stdio loop superseded by ADR-0172) — and this is the first module in the repository built from research that PASSED THE GATE rather than from a plan; every decision cites an E- row in docs/decisions/2026-09-21-agent-interface/. stdio because semantics are identical on both transports, so the difference is operational, and remote costs the entire OAuth 2.1 chain. Two tools, not one, because a server cannot initiate a request — so the alternative is a call held open for minutes and an agent that loses a paid run when its process dies. A resource_link, never embedded bytes: the spec defines embedding and states no maximum size, so "permitted" is not "known to work". The credential stays in the environment, which for stdio is what the spec itself prescribes. The gate refused the first draft of that brief, naming a load-bearing GAP; two more pages went into the same ledger to close it, and resource_link is what came back |
| 0035 | A privacy claim is measured, not asserted — and this repository stays public. The collector's own header said dispatch inputs are visible to anyone with read access. I wrote that from reasoning and never tested it; unauthenticated probing found the topic in none of the readable responses — run metadata, step names, timing and the artifact listing are 200, while the artifact download is 401 and the job logs are 403. A stranger learns a run's shape, not its subject. Wrong in the direction that feels safe is still wrong: a reader who finds one warning false discounts the next. Staying public because this account is on the free plan (checked, not inferred) where going private makes environment secrets ignored — a plan change, not a toggle. The claim is now executable: bin/disclosure.mjs, unauthenticated by construction, exit codes as the finding, and needleChecked so an unchecked thing can never print as a clean one |
| 0036 | The gate can see single-sourcing, and says so without blocking. Twelve checks asked whether a claim was backed; none asked whether it was backed more than once — so a corpus where every unknown rested on one page passed preflight AND --strict, and that corpus was this repository's own, on the day it justified ADR-0031 and ADR-0032. corroboration reports the SHAPE of support: single-source, one-voice (several rows, one host), or independent (several hosts). It never fails on its own — a vendor's own reference is the authority on that vendor — and joins the POLICY_CHECKS so evidencePolicy=strict is where an operator asks for the harder rule. The host distinction is the point: two pages from one vendor are one witness read twice. It immediately contradicted a commit written an hour earlier that called three docs.github.com pages "independent"; they are independent pages, not independent sources, and the check was right |
| 0037 | An empty capture is not mechanically detectable, and here is the measurement. No thin-capture check: emptiness is a question about meaning, and both mechanical proxies measured for it - word count and boilerplate share - rank the one known-empty capture at the wrong end of their scales. A check built on either would be confidently wrong, which is worse than the silence it replaced. partial-render ships instead, because "did something fail to load" is a mechanical question and "is this empty" is not |
| 0038 | A judgement the gate can read, and the reason it must be written down. A reviewer may overrule a corroboration or render finding - in writing, in the corpus, on the evidence row, with a reason the check requires but does not grade. The format already had this shape twice; captures stay byte-immutable, so the note lives on the row, never in the capture's front-matter. Three guards keep it from becoming a mute button |
| 0039 | A prediction is only a prediction if it was chained first. A prior is optional, and when it exists its order is cryptographic, not procedural: research/PRIOR.md is registered by an op: 'prior' ledger entry carrying its hash, only before the first capture, one per corpus - so a prediction written once the answer is known cannot pass for one written before |
| 0040 | --status reads the search vendor's own meter. When the selected search adapter can read it and a key is configured, --status prints the vendor's count beside the local one: used this cycle, left, renewal date, and the account's own hourly limit - from serpapi.com/account.json, which SerpAPI documents as free and uncounted (E-26). Research of the same day found the local line wrong three ways: the 50/hour is the documented plan while this account enforces 250 (U-9), the month is a calendar month while SerpAPI's is a billing cycle (U-10), and the count is one machine's. The read lives in the adapter (NFR-3), copies only whitelisted fields (the payload carries the email, the account id and, in the documented example, the key), and never runs without a key (FR-5). Rejected: staying offline with labels only, an opt-in flag, replacing the local count, and enforcing the limits |
| 0041 | Pinned actions move to their latest Node 24 major, by commit. GitHub removed Node 20 from its runners on 2026-09-23, and checkout, setup-node, setup-python and upload-artifact were pinned to node20 releases running under a forced Node 24. All four move to v7, each resolved to a commit (not a tag object) with runs.using read from that commit's action.yml; every breaking change between the old pins and v7 was checked against these workflows and none applies. setup-node's automatic caching is switched off explicitly in the two workflows that can read an API key. Rejected: stopping at the first node24 major (same review, less runway), tag pins, and relying on the forced runtime. Evidence: docs/decisions/2026-09-26-actions-node24/ |
| 0042 | Key-holding workflows deny the Actions cache; execution protections are the operator's. collect.yml and live-collection.yml declare top-level cache-mode: none - neither uses a cache, both logged Cache mode: write, and a denied cache step logs and continues - pinned by a test that also refuses a per-job override. A workflow execution protection rule (owner, workflow_dispatch, scoped to the two paid files) is recommended as a repository setting. Rejected: replacing the ADR-0033 marker with that rule (it limits who may start a run, not whether the environment exists), per-job cache-mode, extending it to offline-suite.yml, and changing dispatch.mjs for expired artifacts. Evidence: docs/decisions/2026-09-26-actions-sept-changes/ |
| 0043 | The suite runs on ubuntu-26.04 through the ubuntu-latest migration. GitHub moves ubuntu-latest to Ubuntu 26.04 between 2026-10-19 and 2026-11-19 and advises testing first; the offline suite (no key, no spend) gains a temporary ubuntu-26.04 leg - a second Linux image, not a new platform. Rejected: pinning 24.04 (postpones an unwarned break), a one-off run (the image changes weekly), a runner choice for the paid live workflow (not now), and swapping the label early (drops 24.04 while it is still what most runs get). Temporary: remove once ubuntu-latest logs Image: ubuntu-26.04, and by 2026-11-30. Evidence: docs/decisions/2026-09-26-actions-node24/ E-08 |
| 0044 | A preference can name a path on a shared host. prefer matched hosts only, so prefer: github.com gave every page on GitHub the owner's bonus - collect run 36279879229 captured an unrelated repository's run page that way. An entry may now carry a path (github.com/actions/upload-artifact): that path and below, on exactly that host, at a segment boundary; a bare host keeps its meaning. One parser, so every entry point gets it. Rejected: a separate prefer_paths input (collect.yml uses 9 of 10 dispatch inputs), regex or globs (a DoS surface, unreviewable at a glance), ranking path matches above host matches (no need yet), and special-casing github.com (every shared host has the problem). Evidence: collect run 36279879229; fetchproof/test/collect.test.mjs |
| 0045 | The collector fetches pages it is given by URL. A line of collect.yml's queries input that is an http(s) URL is fetched, not searched; with only URLs, nothing is searched. Pages are typed S (the reviewer promotes) and count against max_pages. A dispatched URL is as visible as the topic (GitHub prints each step's environment into the run log), and the input descriptions say so. queriesInput refuses a non-http(s) URL before dispatch, for collect-remote --url and the MCP urls alike. Rejected: a separate urls input (9 of 10 dispatch inputs are used), typing dispatched pages P, and accepting any scheme. |
| 0046 | CI tests every supported Node line, and doctor names a Node that cannot use the proxy. Node supports 22, 24 and 26 on 2026-09-27; CI ran 22 alone, and the keyless transport's proxy support needs 22.21+ or 24+. A node-lines job runs 24 and 26 on Linux, suite requires it, and a test keeps the README's list and the workflow in step. doctor warns (not fails) on a Node that cannot use a configured proxy, and on an odd line below 27. Rejected: raising the floor to 22.21, Windows legs for 24 and 26, and keeping CI on 22. Evidence: docs/decisions/2026-09-27-node-support/ |
| 0047 | Every request the kit makes uses a configured proxy. Node's fetch ignores HTTPS_PROXY unless told, and after dd7569d three paths still went around it: the SerpAPI child, and the commands that call GitHub in their own process (collect-remote, disclosure, the MCP server). A fetching child is started with fetchEnv; a fetching command calls honourEnvProxy() first - in place on 24.14+/25.4+, a restart with the flag on 22.21+/24.0-24.13 (same streams, the child's exit code), a one-line warning older. A startup choice is never overruled, and a value Node cannot parse (it crashes every fetch once the flag is on) is left off and named. Rejected: a child for every GitHub call, documenting the flag, undici internals, always restarting, rewriting a bare host:port. |
| 0048 | The commit gate lets a project's own scaffolding through. While the verdict failed, only research/ could be committed, so a new project's first git add -A was refused - on a fresh collector and on a builder unpacking a remote package - and the .gitattributes that keeps capture hashes valid on Windows (ADR-0020) could not travel during phase 1. Now .gitattributes, .gitignore, AGENTS.md, START_HERE.md at the root, and docs/ARCHITECTURE.md while it is still the empty template, go through; everything else stays blocked. Rejected: the whole map, an editable whitelist under research/, not scaffolding the map, documenting "commit research/ first". |
| 0049 | The MCP server also serves 2025-06-18. A client on the official SDK up to 1.24.0 (2025-12-02) asks for 2025-06-18 and refuses 2025-11-25, and the server answered it 2025-11-25, so it disconnected. Nothing 2025-11-25 changed touches this server, and the specification requires the echo from a server that supports the version (docs/decisions/2026-09-27-mcp-protocol-versions). 2025-03-26 stays out: the results carry structuredContent and resource_link. Extends ADR-0034; its fallback stands. |
| 0050 | A command that runs the kit is quoted, and a project that travels spells it $HOME. Every remote package said node ~/.agents/fetchproof/bin/..., and on Windows an unquoted ~ reaches node literally in PowerShell 5.1, 7.4 and 7.5 without an experimental feature; cmd has no ~; and unquoted commands split on a space in the kit path. Now node "$HOME/.agents/fetchproof/bin/..." (PowerShell, bash, zsh; a space holds), a quoted absolute path for local projects, and a cmd line in START_HERE (%USERPROFILE%). Rejected: keeping ~, %USERPROFILE% everywhere, a PATH launcher, one line per shell (docs/decisions/2026-09-27-kit-path-spelling). |
| 0051 | A command written into a project file is spelled for the machine that reads it. The brief and the timeline said node fetchproof/bin/... and node bin/brief.mjs, which from the project folder named nothing. documentCommand now writes node "$HOME/.agents/fetchproof/bin/..." when the running kit is the standard install, and its real path anywhere else. Rejected: the absolute path always (wrong on the builder's machine), $HOME always (wrong on a checkout), and reading the spelling back from the project's prose. |
| 0052 | A received package says how to re-package it once reviewed. README-FIRST said building needs APPROVED_BRIEF in a manifest that is written once, and gave no way to get a new one. It now prints the preflight and brief commands, and one artifact.mjs create line carrying the package's own identity; state is still derived from the project. Rejected: a --from flag, documenting it only in the kit README, leaving it to the fetching agent. |
| 0053 | The edit gate judges a subfolder cwd by the repository it is in. From src/ the hook found no gate markers and allowed every edit as "not a gated project", while the commit gate (run at the top level) refused the same file. It now takes git's top level when the cwd is not gated, in the cwd's own spelling. Rejected: walking up for markers (can reach another project), rooting at the target file, leaving it. Superseded in part by ADR-0182: a named file is judged by the project it belongs to. |
| 0054 | A duplicated ID fails the gate. A contract with two U-1 rows passed with a warning, and corroboration printed its single-source warning twice. Lookups keep the last row, so every reference to the ID was ambiguous. hygiene/duplicate-id now fails and says how to fix it, and corroboration weighs each ID once. Rejected: keeping a warning, failing only unknowns, deduping findings by text. |
| 0055 | A drafted brief is stamped with what it was drafted from. A brief drafted while the gate failed still said "Gate: FAIL" after PASS, preflight and handoff passed, and brief.mjs refused to redraft an untouched draft. The stamp's body hash lets an unedited draft be redrafted; its inputs hash drives hygiene/brief-stale. Rejected: failing the gate, judging edits by TODOs, comparing with a fresh render, always redrafting. |
| 0056 | decompose maps the project's own topic. decompose --topic x in a project scaffolded for another topic rewrote the map's topic silently, and the brief was titled "Brief - x". Without --topic it now maps plan.json's topic, and a different one is refused naming both; an "Untitled topic" project accepts any. Rejected: warning, renaming plan.json too, overloading --force. |
| 0057 | handoff on an empty collector says nothing was collected. It told a collector with nothing collected that "something did not travel" and to fix it on the collector - itself. The report now carries nothingCollected, and on a collector the CLI names research.mjs; builders are unchanged. Rejected: a new finding name (doctor and the validator branch on the old one), the same wording on a builder, exit 0. |
| 0058 | A capture is never overwritten with different bytes. Capture names are date + title + URL digest, so a same-day re-collection wrote over the first reading and broke its ledger hash when the page had changed. An identical page reuses the file; a changed one goes to .r2.md, which sorts after the original. Rejected: time in every name, refusing a same-day --force, a -2 suffix (sorts first). |
| 0059 | The edit gate denies typing into research/raw/. A Write to a capture or the ledger was allowed as phase-1 work, and a forged ledger line with a correct hash passes preflight. It is now denied whatever the verdict; only editGate.mode=off and GATE_OFF let it through. Rejected: ask, denying only the ledger, tying it to the gate mode. |
| 0060 | A same-day refresh is known by its fetches. A --force refresh on the same day was both "superseded" and a "duplicate". Two same-day rows are a refresh when the ledger records a fetch for each. Rejected: telling them apart by capture file (an unchanged page reuses it), dropping the warning. |
| 0061 | handoff names an unreviewed brief, and does not fail on it. A builder got "handoff OK" for a brief with both judged sections still TODO. The report carries brief: { state, todo } and the CLI prints a note. Rejected: failing the handoff (it answers whether the corpus arrived), saying nothing. |
| 0062 | The line-ending remedy rewrites the working file. Run verbatim after a real autocrlf checkout, the old remedy left every capture CRLF: --renormalize fixes the index only, and git will not rewrite a file it believes unchanged. Each capture is now dropped from the index (git rm --cached) and checked out from HEAD, and the pin is printed only when missing. Rejected: renormalize, touch (no Windows), deleting the file, handoff repairing it. |
| 0063 | Uninstall restores core.hooksPath only while it is still the kit's. An uninstall reset a machine's /opt/myhooks, which the kit had never set, to unset. It is now restored only when it is still the kit's folder, and otherwise left alone and named. Rejected: always restoring, refusing the uninstall. |
| 0064 | An existing .gitignore or .gitattributes gets the kit's rules appended. Scaffolding into a folder with its own .gitignore kept it and dropped the kit's rules, so a .env was staged. The missing lines are appended once under a marked comment; a foreign AGENTS.md is warned about, not merged; doctor and new-project warn kit-rules-missing. Rejected: warning only, overwriting, merging AGENTS.md. |
| 0065 | Firecrawl search spend is counted, and estimated by the documented rule. Four searches cost ~7 credits and the kit recorded none. The adapter reports each search and an estimate of 2 credits per 10 results (E-03, E-14), which reaches the usage log, the run summary and --status, labelled as an estimate. Rejected: reading the balance per search, counting searches only, pricing an empty search at 0. |
| 0066 | A refused or empty search is reported, and the user agent stays honest. DuckDuckGo served its bot check, the keyless adapter parsed it as zero results with ok: true, and the run said nothing. The check is now a failed search with its reason, and every search logs what it found (an empty one also goes to .failures.jsonl). Rejected: a browser user agent (bot-detection evasion), retrying, reporting failures only. |
| 0067 | A search result must carry the query before it costs a scrape. SerpAPI returned only generic "postgres" pages for a query about pg_sync_replication_slots, and the kit scraped the home page. A result now needs half the query's distinctive terms in its title, snippet or URL, or a preferred domain; an all-miss search spends nothing and is reported. Rejected: one term, all terms, no prefer exemption. |
| 0068 | The relevance floor is two terms, not half (supersedes ADR-0067's threshold). Measured on the 38 pages real searches found here, "half" kept 2; min(2, half) keeps 30, rejects no cited evidence, and still rejects the Postgres home page. A corpus-wide test now guards it. Rejected: one term, rare-term weighting. |
| 0069 | The brief's stamp covers the map, the ledger and the verdict. A brief drafted while only the map was incomplete still said "Gate: FAIL" after the gate passed, with no stale warning. The hash now covers map rows and ledger/capture counts, and the stamp records the verdict, which preflight compares with its own. Rejected: hashing all of research/, computing the verdict inside hygiene. |
| 0070 | The line-ending remedy runs in Windows shells. It printed printf (in neither cmd nor PowerShell) and trailing # ... comments (file names in cmd), for a remedy meant for Windows. Every command is now a plain git command, and the pin is two lines to add in an editor. Rejected: echo >> (UTF-16 in PowerShell 5.1), three shell variants. |
| 0071 | The suite runs from the repository root. Started from any other folder, selftest.mjs crashed at import (a test reads fetchproof/schemas/... relative to the cwd), so no test ran and no count was printed. The runner now changes to the repository root first, after resolving a relative result file against the caller's folder. Rejected: rewriting every cwd-relative test path to KIT_ROOT. |
| 0072 | The commit hook finds the home directory without HOME. Under set -u, an unset HOME stopped the hook with "parameter not set", so every commit was refused by a shell error. It now asks node for os.homedir(), as lib/machine.mjs does. Rejected: ~ (dash does not expand it without HOME), ${HOME:-} (the kit and config would not be found where machine.mjs finds them). |
| 0073 | An unused Firecrawl key is named at collection time. With the key set and no CLI on PATH, a run fell back to keyless and said only "no CLI on PATH". research.mjs and decompose.mjs now print a note that the key is unused, with the install command. Rejected: refusing the run, calling the HTTP API with the key directly. |
| 0074 | The reviewer may be an agent, and the package says who declared it. The package said "a human reviewed it", but nothing checks who did the three review steps, and AGENTS.md gives them to the agent. A Reviewed by: agent|human line in the brief becomes review.by (format 1.1.0); approval is unchanged. Rejected: requiring a human, making the declaration a condition, a --reviewed-by flag, renaming the state. Amends ADR-0032. |
| 0075 | Lighter records for small changes. A commit outside the declared code paths may give its report as one line per part (the got-wrong line stays), and an ADR is owed only for a design choice with a rejected alternative, not for test mechanics, wording or a one-remedy bug fix. Rejected: dropping the got-wrong line, scaling by diff size, dropping dual conformance. |
| 0076 | Nothing from outside the project is read as a capture or packaged. git stores symlinks; createArtifact followed links under research/ and docs/ and copied outside files, a private key included, into the zip. realInside resolves real paths; packaging refuses (exit 3), and readCaptures records a blocking capture-outside. Rejected: skipping links, refusing all symlinks. |
| 0077 | Every capture is read or named. A 100 MB file in research/raw/ crashed every corpus reader (RangeError in the similarity sketch), a 600 MB or unreadable one was skipped silently, and an unreadable ledger-named capture crashed preflight. Captures over 10 MB (checked before reading) and unreadable ones are now blocking findings. Rejected: warning only, streaming large captures. |
| 0078 | The Python floor is 3.11, and CI tests exactly the floor. The kit promised 3.12+ while the runners pass on 3.10-3.13 (measured), and the policy test let README, code and CI drift. The floor is now 3.11 everywhere, and a test keeps the three in agreement. Rejected: keeping 3.12, 3.10 (end of life October 2026), a four-version matrix. |
| 0079 | A rewrite replaces the file whole, or not at all. writeText emptied its target before writing, so a write cut short (a full disk, a quota, a kill) destroyed the file it replaced: 6,300 bytes became 4,096 of the new text. It now writes a scratch dotfile and renames it over the target, keeping mode and symlinks. Rejected: a dependency, fsync, per-caller backups. |
| 0080 | The suite runs in a repository checkout, and refuses a deployed kit. Run from ~/.agents/fetchproof, 80 tests that read repository fixtures failed on a healthy install. selftest now names the missing .github/workflows, docs/adr and AGENTS.md, points at doctor, and exits 2. Rejected: skipping absent fixtures, leaving test/ out of the deploy, porting every test. |
| 0081 | The gate's override log stays on the machine that took the override. The .gitignore files un-ignored research/overrides.log while repo-hygiene and the artifact validator treat it as machine-local. It is now ignored everywhere. Rejected: letting it travel as an audit trail, which the overriding party decides whether to commit. |
| 0082 | A page past the size limit is refused, not kept cut short. Every transport child ran under spawnSync's 1 MiB default output buffer, so pages just over 1 MiB were lost to ENOBUFS, Firecrawl ones after being paid for. Pages are now limited to 16 MiB and child output to 128 MiB, each overflow named. Rejected: keeping a truncated page graded partial; removing the limit. |
| 0083 | JSON nested deeper than 256 levels is refused, in both languages. The Node reader recursed, so release validators reported a stack overflow, and Node took thousands of levels where CPython's canonicaliser gave out near 300, so the conformance runners disagreed on deep packets. Both now refuse past 256, and a test proves they agree on both sides of the limit. Rejected: 512 (Python fails below it), an iterative Python with no limit, raising Python's recursion limit. |
| 0084 | A project records how it spells the kit, and later files reuse it. new-project --kit wrote $HOME/.agents/fetchproof into every scaffolded file, but the brief and the timeline, drafted later, spelled the running kit, so a brief drafted from a repository checkout named a path that existed only on that machine (MoonAliza, 2026-09-28). The scaffold now records kitPath in research/kit.json, and documentCommand prefers it. Rejected: reading the spelling back out of AGENTS.md prose, always writing $HOME, documenting a manual edit. |
| 0085 | Phase 0 searches a compound topic part by part. Searched whole four times, MoonAliza's three-question topic mapped forum threads and a coffee-scale blog. subject: part, part; part is now one search per part (a one- or two-word part carries the subject; at most 6 parts), and the candidates are interleaved one per part so every question reaches the map's 20. A plain topic keeps its four searches. Rejected: per-subtopic search before classification, four searches per part, splitting on "and", advising short topics. |
| 0086 | Exhausted Firecrawl credits switch the rest of a run to http-keyless (default superseded by ADR-0129: opt-in with --fallback). Firecrawl answers 402 and its CLI prints only "Insufficient credits ...", so the adapter declares creditsExhausted and research switches once, re-fetching the refused page, reported everywhere and recorded per capture in the ledger; --no-fallback turns it off. Rejected: Tavily (C-6 re-checked, unchanged), Firecrawl's keyless tier, matching "402", falling back on any failure, a silent switch. |
| 0087 | A quote in a claim must occur in its capture. An EVIDENCE Finding may carry [quote: ...]; citations checks each fragment occurs in the row's capture, in order, exact after NFKC and Markdown/quote/dash/whitespace/case normalization - blocking when not found, a warning under three words, nothing for unanchored prose. Rejected: a 0.92 fuzzy fallback, checking quotation marks in prose, judging support, a new EVIDENCE column. |
| 0088 | A local Chromium is a fetch transport, chosen by name. --transport browser renders pages with --headless --dump-dom through the keyless extractor, passes the proxy as --proxy-server, drops the sandbox only as root, and treats Chromium's own error page (exit 0) as a failure; it does not search, and it is the preferred credits fallback when installed. A GitHub Discussion that answered keyless with 403 rendered in full. Rejected: a driver library, --ignore-certificate-errors, trusting the exit code, auto-selection. |
| 0089 | The kit measures its citations without a judge, and names what needs one. measure.mjs reports link works, quote anchors found, full captures and closure strength per corpus, names Fact Check as needing a judge, and never gates. First run over 22 corpora: docs/measurement-2026-09-28.md. Rejected: a model judge, a gating threshold, one folded score. |
| 0090 | The corpus exports as WARC 1.1 resource records. export-warc.mjs writes a warcinfo record, then per unchanged capture a resource record (the kept text) and a metadata record (transport, completeness, ledger hash and seq), sha256: block digests, IDs derived from the ledger, one gzip member per record; warcio reads every corpus's export. Rejected: response records, WACZ, random IDs, sha1/base32 digests, exporting an edited capture.|
| 0091 | Decompose shows the outlines of its material, without a model. The map gains ## Outlines seen in the material: the ##/### headings of every gathered page already captured, furniture and repeats dropped, unjudged, no extra fetch - STORM's table-of-contents step, whose own ablation credits reading related material over perspectives. Rejected: an LLM naming perspectives or asking questions, fetching Wikipedia tables of contents, auto-adding headings as subtopic rows.
| 0092 | The browser keeps its sandbox on Ubuntu 23.10+, by choosing the binary Ubuntu allows. Linux search order puts Chrome stable (/opt/google/chrome/chrome, which Ubuntu's AppArmor profile allows user namespaces) before Chromium builds; a "No usable sandbox" crash names Chrome stable and CHROME_DEVEL_SANDBOX. Rejected: retrying with --no-sandbox, lifting the runner's sysctl, setting the env var silently.
| 0093 | Altered evidence is never committable, whatever else is staged. Amends ADR-0048: a commit confined to research/ passes a failing verdict only on completeness findings; provenance/body-unmodified, chain-intact, ledger-unparsed and fetch-entry-exists block it, with the restore-or-re-collect remedy. Rejected: blocking every provenance finding, the prior's order rules, relying on handoff, the edit hook.
| 0094 | Phase 0 spends its scrape budget on the likely owners first. scrapeOrder puts pages on the hosts docsHosts ranks as owners first (by score), then the rest, each in search order; the floor, cache, budget and the map's list are unchanged. Found on MoonAliza: both scrapes went to a spec and a forum thread while the map named docs.ollama.com. Rejected: owners only, per-page scoring, re-ordering the list.
| 0095 | A collector that chose a keyless route is configured, not broken. doctor passes firecrawl-cli/firecrawl-auth on a collector whose machine config or FETCHPROOF_TRANSPORT explicitly chose a transport other than firecrawl-cli; with nothing chosen, or Firecrawl chosen, they still fail. Rejected: warn for every collector, pass whenever a transport works, counting a per-run flag.
| 0096 | On a shared host, the owner is the account. Phase 0 ranked github.com the likely owner (7) across five unrelated accounts, so every scrape went to strangers' repositories and none to docs.ollama.com. ownerOf keys github.com, gitlab.com and similar hosts by account; docsHosts and scrapeOrder use it. Rejected: dropping shared hosts, account+repository keys, inferring shared hosts from results. Amends ADR-0094. |
| 0097 | A floor-skipped page on the likely owner is named, not scraped. Phase 0 skipped docs.ollama.com's terse "OpenAI compatibility" page, the owner's, and said so only in a log line. The map now lists such pages under "Skipped on the likely owner" with the way in (plan.json urls), when the top owner scored at least 3. Rejected: trusting the owner past the floor (it re-admitted RR-9's postgresql.org home page), a one-term floor on the owner, loosening matchesQuery. |
| 0098 | A topic part that refers back carries the subject. "..., and which encodings they also mask" was searched alone and found an IDL encoding manual. A part with they/them/their/it/its/this/these/those now carries the subject, like a one- or two-word part; a long part without one is still searched alone. Rejected: always prefixing the subject, resolving the pronoun, refusing such topics. Amends ADR-0085. |
| 0099 | A render warning is for captures a closure cites. partial-render told the reviewer to confirm the text they cite on a phase-0 page no unknown cited - an instruction with nothing to act on, repeated for every such page. It now applies only to a capture behind an EVIDENCE row some unknown cites. Rejected: warning on every capture, reading "context only" prose, deleting context rows. Amends ADR-0038. |
| 0100 | A page that answered 404 or 410 is not fetched again within the refresh window. A wrong plan URL was paid for twice in ten minutes, and a remembered 404 could take the only slot of a small budget. recentlyGone reads the ledger's last word on the URL; the loop sets it aside before the budget, --force retries. Transient failures are still retried. Rejected: retrying everything, remembering every failure, editing the plan. |
| 0101 | macOS is best-effort, and CI has no macOS leg. A red leg nobody fixes teaches people to ignore red, and an "offline-only" leg is the same leg, because the CI suite is the offline suite. Expires when someone commits to fixing macOS failures, or a macOS-only regression is reported |
| 0102 | Decision projects are indexed, never moved or deleted. docs/decisions/README.md lists each nested project with what it backs and a status: active, superseded or reference. An archive/ folder would break about 60 references; deleting would destroy provenance. Expires when the index passes about 100 rows, or a capture must leave for licence or privacy reasons |
| 0103 | A topic part that opens with "the" carries the subject, as a pronoun part does (extends ADR-0098). "The bot limiter", searched alone, found audio limiters. Giving the subject to every part stays rejected (ADR-0085): a private name makes a self-contained part worse |
| 0104 | SearXNG is a search-only provider, chosen explicitly (--search-transport searxng, SEARXNG_URL/searxngUrl). A 403 is named as "enable json in search.formats"; rows without a url are dropped. Automatic selection, a public-instance default, and bundling SearXNG are rejected |
| 0105 | The keyless transport refuses a binary response (PDF, image, archive) instead of keeping it graded partial. Read as text it is unrecoverable and can hold no quote; one PDF put 2.3 MB of it into a corpus. The refusal names firecrawl-cli, which converts documents. Base64 storage and an in-kit PDF parser are rejected |
| 0106 | The Wayback witness is opt-in and lookup-only. research --witness asks the Availability API for the snapshot closest to each newly collected page and appends the answer to research/witnesses.jsonl, outside raw/ and the ledger, so it can never fail a capture. On by default, Save Page Now, a ledger record and a gate check are rejected |
| 0107 | The review is the agent's, and the state says REVIEW_REQUIRED. Format 2.0.0 renames HUMAN_REVIEW_REQUIRED and retires review.by: "human"; the validator still reads 1.x packages and reports their old state under the new name. Supersedes ADR-0074 in part. Keeping the name, dropping 1.x, and keeping human are rejected |
| 0108 | A local run may waive unsupported tests; CI never does. FETCHPROOF_ALLOW_UNSUP=1 lets a run whose only blockers are UNSUP exit 0, listing them and ending "NOT a full pass"; ignored when CI is set. Default non-blocking UNSUP, skipping the Python tests, and honouring it in CI are rejected |
| 0109 | One helper decides whether a recorded path may be read. projectFile answers outside / missing / not-file for a ledger or Raw-cell path; verifyLedger and exportWarc use it. Moving corpus, artifact and evidence-context is deferred until a new reader or a defect touches them. Moving all now and a lint are rejected |
| 0110 | A redirect may not lead the collector into this machine's network. The keyless fetch follows redirects hop by hop and refuses one into loopback, private, link-local (the metadata endpoint), CGNAT or localhost - unless the URL asked for was itself internal; FETCHPROOF_ALLOW_INTERNAL_REDIRECTS=1 lifts it. The browser transport and DNS rebinding are deferred with triggers. Refusing every internal URL, flagging after capture, and no opt-out are rejected |
| 0111 | A deploy mirrors only into a folder that holds a kit. deploy refuses, touching nothing, a FETCHPROOF_HOME that holds files and is neither a kit (lib/core.mjs, bin/gate.mjs and bin/preflight.mjs) nor the recorded deployment; --dry-run says the same and install exits 2. A --force flag, merging, a new marker file and a single marker are rejected |
| 0112 | The kit's hooks directory hands every hook on to the repository's own. git runs hooks from one directory, and the machine-wide core.hooksPath had silenced every repository's .git/hooks (LFS pre-push included); githooks/ now has a pass-on file for each of git's 28 hook names, and pre-commit hands on after the gate allows. A replaced global hooks directory and future hook names are deferred with triggers. Disclosure only, per-repository install, and a subset of names are rejected |
| 0113 | The hand-on runs what git would have run without the kit. The hooks folder the install replaced runs when there was one (recorded as fetchproof.previousHooksPath in git's global config), else the repository's own; one githooks/hand-on.sh sourced by every hook. Supersedes ADR-0112's "global hooks directory" bullet. Running both, and a JSON reader in sh, are rejected |
| 0114 | The keyless fetch judges the address it connects to. The fetch child wraps dns.lookup, which Node's fetch calls at connect time, and refuses an internal answer (EINTERNAL), so a second, different DNS answer cannot slip past; an internal URL asked for and the proxy's own host stay exempt. Supersedes ADR-0110's DNS-rebinding bullet. Rewriting onto https.request and connecting to the judged IP are rejected |
| 0115 | The browser transport checks a URL's redirects before Chromium starts. redirectTarget follows the server redirects with the keyless fetch and its guards, without reading the page, and a hop inward is refused before the browser runs. Page-made navigation stays open; the full fix, a local guard proxy for Chromium, is deferred with a trigger. Supersedes ADR-0110's browser-transport bullet. DevTools interception and a final-URL check (no final URL exists) are rejected |
| 0116 | One kit version, stated once. KIT_VERSION in lib/core.mjs (0.9.0); the MCP server announces it, doctor reports it, CHANGELOG.md has its entry, a release is tagged v<version>, and a test holds them together. package.json, git describe, and a VERSION file are rejected |
| 0117 | Feature freeze from 0.9.0. Only bug fixes, deferred gaps whose trigger fires, documentation and tests, and keeping up with vendors, Node and git; a new command, flag, transport, provider, config key, format or check needs an ADR lifting the freeze for it. Expires on the owner's 1.0 decision. No freeze, and a test-enforced freeze, are rejected |
| 0118 | The browser renders through a guard proxy that judges every request. Chromium is pointed at a loopback proxy the kit runs in a child process (browser-guard.mjs), loopback bypass off; every request and CONNECT is judged by the keyless rule and pinned to the judged address, only the asked host:port exempt; a configured proxy is the guard's upstream; a page that landed on a refusal is reported by name. Supersedes ADR-0115. DevTools interception, keeping the pre-check, and hostname-level exemption are rejected |
| 0119 | The render timeout starts at the browser's first request. The child arms the kill at launch and re-arms it when the guard sees the first request, so the launch is allowed as long as the render and no more; startupMs is reported, and a timeout says what the launch cost. Found on CI: Chromium's startup took 4 to 43 s before any request, out of the render budget, and the page was killed before it had been asked for. Rejected: counting the launch inside the timeout, a launch-timeout setting (frozen), a larger fixed timeout, curing the runner. Superseded in part by ADR-0188: the launch allowance runs from the guard child's own start. | 2026-10-01 |
| 0120 | A red suite blocks a commit in the kit's own checkout. Lifts the freeze for one check: in a checkout holding the kit's four markers, a commit staging anything under fetchproof/ is allowed only when bin/selftest.mjs reports green through its result file; an unsupported test is not red, a runner with no result is a block, docs-only commits owe nothing, and the hook's watchdog is 1500 s there (amended by ADR-0142: 65 minutes, 4020 s, and the operator's timeout governs both; superseded in part by ADR-0183: the suite runs in a scratch repository built from the index being committed). Found 2026-10-01: two commits on a red suite in one afternoon, both from a shell chain that misread the exit code. Rejected: prose, CI only, every gated project, every commit, a separate hook. | 2026-10-01 |
| 0121 | An ill-typed posture key resolves to its restrictive state. A failOpen, evidencePolicy or editGate.mode that is present and not of its type resolves to fail-closed, strict and hard-block - as role already resolves to unknown (ADR-0023) - and doctor names it (config-ill-typed, the one item this ADR lifts the freeze for); the hook's sh reader agrees on failOpen. Absent keys keep their defaults. Found 2026-10-02: {"failOpen":"false"} read as fail-open in both readers. Rejected: the lax default, treating the whole config as unreadable, coercion, naming without tightening. | 2026-10-02 |
| 0122 | The collector refuses to spend on a broken chain. assertAppendable is the one function that establishes a ledger can be appended to - unparsed lines, a torn tail, and now a chain whose seq/prev/entrySha256 no longer link (LEDGER_CHAIN_BROKEN, the one item this ADR lifts the freeze for); runResearch asks it before the first fetch, beside the vendor-CLI refusal, dry runs included, and appendFetch again under the lock. The remedy restores the ledger that verified; the kit never rewrites hashes. Found 2026-10-02: the collector spent and appended seq 4 onto a broken chain that handoff then refused. Rejected: leaving it to handoff/preflight, repairing the chain, refusing only at the record, verifying bodies before the run. | 2026-10-02 |
| 0123 | The suite describes the kit, not the machine it runs on. selftest.mjs gives the run a scratch home and removes every FETCHPROOF_* variable but the ones that describe the host or the run, and the vendor keys, before the first test: the operator's machine config, deployed kit, CLI login and keys were read by the tests that spawn kit commands, so a strict posture config turned 18 tests red and this container's http-keyless config hid two cli tests that are red on any host with the Firecrawl CLI installed and not logged in. Rejected: per-test isolation only, a run-wide FETCHPROOF_CONFIG, documenting a host requirement, stripping FETCHPROOF_BROWSER. |
| 0124 | The guard drops the browser's own service traffic. Rendering a plain loopback page, Chromium 141 made nine connections no page asked for - the component updater's clock (with a key), the default-search preconnect, accounts.google.com, the Cloud Messaging check-in and push channel - all carried as external; flags, Playwright's feature disables, profile preferences and endpoint overrides were each measured and left most of them. BROWSER_SERVICE_HOSTS names them, a request to one is dropped (403, DROP_MARKER, logged dropped), never judged, carried or counted as a refusal; the page asked for is exempt by host and port. Rejected: flags and preferences alone, preferring chrome-headless-shell, refusing them as internal addresses, accepting the traffic. |
| 0125 | A request before the page's own is the browser's. The render's guard (pageFirst) drops every request until the host and port asked for has been requested: CI's Chrome stable preconnected to www.gstatic.com 256 ms into a render of a loopback page, before the page, and that host is also one real pages use, so ADR-0124's list could not settle it; the order can. From the page's request on, only the named service hosts are dropped. Rejected: listing www.gstatic.com, tolerating a tunnel that carried no bytes, a time window. |
| 0126 | A long quote of few words is not short. quote-too-short warns under three words AND under 40 characters (in every fragment since ADR-0162; across the fragments before): two rows of the kit's own decision project quoted a 130-character dependency map, one whitespace-separated "word", and were warned as anchoring almost nothing - the reason was false, and a warning wrong about its own reason gets every warning dismissed. Amends ADR-0087. Rejected: splitting words on punctuation (v1.25.2 would pass), exempting code-shaped quotes (judging prose), leaving it. |
| 0127 | A search for closed unknowns is not run again. A plan query whose why names unknowns that are all CLOSED is skipped and said (terminal, results, dry run); --force runs it, as it re-fetches a page. Found running the kit five times over MoonAliza's finished projects: one query would have paid two credits on every run forever. A bug fix under the freeze. Rejected: a search cache file (new format, and a cached search decays), deleting the query by hand (the plan is the record), running it and saying the cost. |
| 0128 | A binary file is outside the secret scan. Content with a NUL byte in its first 8000 bytes (looksBinary, git's own rule) is skipped by scanForSecrets and counted in its coverage line, and by the package validator as a body over 512 KiB is: doctor on a Windows home folder had reported fourteen committed keys, every one the PEM marker OpenSSH and libssh2 parse or random bytes spelling sk-. A .pem is text and stays caught. Rejected: scanning binaries as before, exempting one pattern inside binaries, skipping by extension, decoding UTF-16 by its byte-order mark (a new capability under the freeze). |
| 0129 | An exhausted account stops a local run; the fallback is opt-in. Without --fallback, a run that detects exhausted Firecrawl credits stops: the remaining searches and pages are reported uncollected, never failed, the CLI prints the two ways forward (top up and re-run; or --fallback, free transports) and exits 2 so the operator decides. The unattended collectors pass --fallback and keep ADR-0086's switch; --no-fallback still means the default. Lifts the freeze for that one flag. Rejected: keeping the switch as the default, a config key, stopping the hosted collector, trying every remaining page on the exhausted account. |
| 0130 | The kit is licensed under PolyForm Shield 1.0.0; its captured pages are not. Code, tests and docs under Shield (every purpose except a competing product), chosen by the owner over Apache-2.0 on the evidence of docs/decisions/2026-10-02-licence-and-captured-pages; the pages under every research/raw/ excluded by NOTICE, REUSE.toml and a LicenseRef-Captured-Page marking, in the archives' words - copyright stays with the owners, no licence granted, no fair-use claim. Rejected: Apache-2.0 and MIT (resale), PolyForm Noncommercial (an agent's research for a company is commercial), Business Source and Elastic (a product company's machinery), keeping all rights reserved, removing the captures, a per-capture notice. |
| 0131 | The installer reports a repository-local hooksPath it cannot gate. installCommitGate probes the directory it runs in and returns displaced: { cwd, local } when that repository sets its own core.hooksPath (husky, lefthook, simple-git-hooks, pre-commit); install-hooks.mjs says so on stderr with the remedy and still installs, exit 0, because the gate is machine state and every other repository gets it. Doctor keeps recording the override at check time. Lifts the freeze for that one check. Rejected: refusing the install, exit 1, searching the disk for repositories, chaining behind the local hook. |
| 0133 | The exhaustion policy has one owner, and the line-ending fold has one home. lib/credits.mjs holds a run's fellBack/stopped state, the accounting and the one failure-log row, and runResearch asks it which provider serves each call; core.foldLineEndings (CRLF and lone CR to LF) is the fold for what the kit writes and compares - collect, gate, brief - while provenance.isLineEndingRewrite keeps CRLF-only, because autocrlf never makes a lone CR and a wider fold would accuse a checkout-rewritten capture of tampering; the release layer keeps its own. From the architecture pass of 2026-10-03. Rejected: policy left inline with the record blocks deduplicated, one fold everywhere, a CRLF-only core fold, calling the fold change "preserved". |
| 0134 | doctor reports a hand-on to another copy of the kit's gate. handOnState classifies the hooks folder the install replaced (ADR-0112) as none, missing, kit or other, and gateHealth warns on kit and missing with the one-line remedy; the maintainer's machine handed every commit on to the previous implementation's gate, which crashed and failed open while doctor said READY (2026-10-03). Lifts the freeze for that one check. Rejected: refusing the hand-on inside the sh hooks, not recording a kit folder at install, warning on somebody else's hooks too. |
| 0135 | One search session asks the providers, for both coordinators. lib/search-session.mjs owns the asking - the merge, the one degrade to the fetch provider, the rate-limit patience, the meters, the failure rows through an injected sink, and the refuse-before-spend readiness gate - and runResearch and decompose both call it; read side by side on 2026-10-03 the two copies had drifted in six places (register G8, H4, J4). One accounting rule: every ask counts what the adapter reports, every failed ask is a failure. Lifts the freeze for that one module. Rejected: extracting the meters only, one discovery module, leaving two copies, a session that writes the failure log itself. |
| 0136 | One page's identity is its host and path, and the merge is a decision. urlKey treats https://example.com/api, /api/, http:// and www. as one page and pays once; the rule of 2026-09-27 (a collect run paid twice for two spellings) recorded now that the output-reliability audit's G6 argued against it as if it were an accident. Stands; a seen-set skip and a cache hit name the spelling that was fetched. Rejected: keeping scheme and trailing slash with redirect-learned aliases (no transport records a redirect chain today), asking before reusing a cache entry. Trigger: a ledger that records every fetch's final URL. |
| 0137 | A browser capture records the origin's answer, or says that it could not. Chromium's --dump-dom reports no HTTP status, so an origin's 403 page was graded full (audit G5); the transport reads the status and final URL from Chromium's own net log (--log-net-log), the collector's existing >= 400 rule then refuses the error page, and a capture whose status the log did not give is graded partial naming the unobserved status. Lifts the freeze for that one change. Rejected: a title blacklist, grading every browser capture partial, DevTools instrumentation, reading the status through the guard (it sees only plain HTTP). |
| 0138 | A brief with no draft stamp keeps its approval, and hygiene says so. Eleven briefs here predate ADR-0055's stamp; the G1 fix kept their approval, in the map alone. hygiene/brief-unstamped now warns on a drafted brief with no stamp, naming the remedy (a redraft with --force), and the stamp is found wherever it stands - anchored to the end of the file, a line appended after it had re-approved a stale brief. Lifts the freeze for that one rule. Rejected: refusing approval to the eleven, stamping them by hand, leaving it in the map. |
| 0139 | A Firecrawl scrape is a live fetch, and a cached answer says so. The vendor serves a cached copy up to two days old by default (maxAge), and the adapter asked for nothing fresher, so a capture stamped with today's date could be yesterday's page; every scrape now passes --max-age 0, and cacheState/cachedAt are recorded when sent. Rejected: --max-age set to the refresh window, re-dating retrieved to cachedAt, a warning on a cache hit. |
| 0140 | The page's source text is kept beside the capture. A capture was the converted Markdown alone, so a conversion defect (G3) was permanent and the ledger certified it; every transport now returns the text it converted as source, the collector writes it LF-folded as <capture>.source.html, the ledger entry names and hashes it, verifyLedger checks it like the capture, and it is never a capture itself. Lifts the freeze for that one file and two ledger fields. Rejected: wire bytes untouched, the source inside the capture, an opt-out flag, re-fetching. |
| 0141 | The installer records no hand-on to another copy of the kit's gate. handOnState moves to installer.mjs (doctor re-exports it) and installCommitGate records no folder it classifies as kit, on a first install or a re-install from the state file: doctor's one-line remedy (ADR-0134) had lasted one session, because the next install-hooks run wrote the old kit's folder back from the install state and its gate blocked commits again (the operator's machine, 2026-10-04). Supersedes ADR-0134's rejection of this; a missing folder stays recorded. Rejected: doctor writing the install state, a two-step fix line, a forget flag, refusing missing too, guarding the uninstall restore. |
| 0142 | The suite's budget follows the gate's watchdog. The commit gate's suite budget (ADR-0120) is 65 minutes, twice the slowest honest run measured on a supported host (1825 s, the operator's Windows PC, where the 20-minute budget blocked every kit commit on a green tree), and FETCHPROOF_GATE_TIMEOUT now governs both layers: the suite gets the watchdog less the 120 s the hook allows the rest of the gate, never more than half a short one, so the gate - not a posture-decided watchdog kill, which a fail-open machine allowed - reports first; both layers read the variable in one grammar (the hook names and ignores a value outside it), 0 leaves the default, the runner is stopped with SIGKILL, and the hook's watchdog in the kit's checkout is 4020 s. Amends ADR-0120; a bug fix under the freeze. Rejected: scaling with a host measure, raising the default alone, passing the setting alone, a new key, dropping the node budget, reading timeout(1)'s whole grammar in node, the hook exporting its watchdog. | 2026-10-05 |
| 0143 | A passing handoff keeps the gate's cautions. PASS reports configured checks, disclosed unknowns remain, and brief/audit/package prose retains every evaluated warning reason and remedy. The shared pure renderer lives in render.mjs; audit fingerprints include the rendered evaluation so changed cautions earn a new immutable version. MCP approval points to those documents. Public outcome effects remain unmeasured. Rejected: a seventh brief section, duplicate renderers, counts alone, guessed subtopic filtering, rewriting history, a new machine warning contract, semantic judgment or changing warning severity. |
| 0144 | Audit versions follow the latest observation. Only a consecutive identical latest fingerprint is reused; A -> B -> A earns three chronological immutable snapshots so the default bundle retains the current warning state. Clarifies ADR-0143 without changing formats or authorization. Rejected: repointing latest backward, rewriting historical audits, re-evaluating during bundling. |
| 0145 | The kit is measured against a strong prompt before it grows. A matched-arm comparison — the kit workflow versus a strongly prompted agent with the same tools, models and budget — over twelve tasks (four per domain, including paid collection, an interruption and a cross-machine handoff), scoring unsupported claims, defects, truthful reporting, successful handoffs, review effort, time and cost on actual outcomes plus transcripts; protocol and rubrics frozen before any run, simplification per mechanism pre-registered. Evaluation only: ADR-0117's freeze and ADR-0089's semantic boundary stay in force. Rejected: a weak baseline, document-tidiness metrics, expanding the product before measuring, ending the comparison at first completion. |
| 0146 | The kit ships a skill set and a role-aware router. Lifts ADR-0117 for one item: fetchproof/skills/<name>/ deployed by install.mjs to every skill root as siblings of research-first and --into the project; the recorded SkillsMDs baseline with renames and marked compatibility adaptations (Apache-2.0), thirteen kit skills, and skill-router, which routes by role and gate state and never sends a builder to a collecting skill (a test holds its table). Owner replies and prose duties remain review responsibilities. Rejected: a router command or key, model routing in the kit, a plugin, nesting inside research-first, an unrestricted rewrite of received text, a ~/.agents/skills mirror. Superseded in part by ADR-0190: lead-orchestrator is the owner's 2.0, and auto-build's ledger pointer names its references/run-ledger.md. |
| 0147 | A desktop panel on 127.0.0.1 over the kit's own commands. Lifts ADR-0117 for one item: bin/panel.mjs opens a local page for keys, doctor, update, the topic and the builder hand-off. It runs doctor, handoff, preflight and install with fixed arguments; ADR-0148 adds capped validated-request collection, not a general collect command. The search key goes to the machine config, never a repository; Host, Origin and a per-launch fragment-initialized token guard requests. No browser-history or terminal-output privacy guarantee is made. Reopens ADR-0031 for the operator, not the .exe. Rejected: Electron or Tauri, a SEA .exe (packaging on top of this), collect buttons, a PATH launcher. |
| 0148 | The collector serves builder requests. Lifts ADR-0117 for one item: a builder pushes research/requests/<id>.json (one fact, owner pages or queries), and the panel's auto-collect, on a collector only, pulls, validates it as untrusted input (no internal URL, no over-budget request), adds the contract row and plan entries, runs the same research.mjs and preflight, and commits the corpus with its ledger and pushes, with <id>.result.json beside the request. Per-request and daily page caps; it pauses when credits run out and never passes --fallback. Amends ADR-0147 decision 3. Rejected: the builder collecting or editing the contract, trimming, auto-fallback, approve-each, a queue service. Deferred: the MCP tool, GitHub Actions. Amended by ADR-0173 (decision 4, last sentence: a topic request waits for an approved topics folder). |
| 0149 | Collection accounting travels on a private child channel. Repairs auto-collect's page cap and exhaustion pause with one received and validated internal Node IPC report; query text, truncated diagnostics and exit zero cannot supply accounting. Unmeasured attempts reserve their remaining allowance and pause with actual spend unknown; continuation retains prior measured spend once, and a valid report survives failed exit/interruption. Logs/detail are bounded and UI labels say used or reserved. Rejected: another regex, unlimited logs, unknown spend treated as zero, a new public measurement interface, double charging continuations. |
| 0150 | Complete review and Git corpus ownership in builder handoffs. Maintenance clarification: intact handoff/PASS, classified MAP, rewritten Findings and an authored answered/current brief are the existing role-agnostic approval conditions. The collector records Git request review/closure; collected means delivery. Preserves ADR-0052 local received-package review and ADR-0138 authored unstamped compatibility. Corrects keyless/readiness advice without changing public interfaces or machine config. Rejected: presence-only permission, a role predicate, revoking compatibility, builder-written collector review records, another fetch for a review-only gap. |
| 0151 | Bounded Git output cannot authorize delivery. A private truncation flag accompanies the bounded child byte buffer; every Git answer used for path, scope, eligibility or a delivery fingerprint is refused if incomplete. Invalid ahead counts and missing quiet-diff exits also block delivery. Collector trust is recorded before a partial fingerprint attempt, preserving spend and continuation after refusal or restart. Rejected: parsing a retained prefix, unbounded buffers, adding a streaming digest without complete path/status handling, discarding the partial result. |
| 0152 | Admission is charged before the collection child starts. Existing atomic daily-spend and pause fields protect the admitted allowance before spawn, so collector termination cannot silently forget it. A valid private receipt refunds unused allowance once; unknown/spawn-failed attempts remain charged and paused. Continuation counts earlier attempts once; cross-day completion never refunds yesterday against today. Supersedes only ADR-0149's unnecessary-durable-admission claim; no public format/key/flag. Rejected: post-child-only charging, dirty-tree protection, fake zero on restart, another public file/key, negative recordSpend and cross-day refund. |
| 0153 | Preflight JSON reports ungated scope without approval. Repairs text/JSON scope parity in the CLI alone: after existing argument and check-name validation, a cwd with none of the four gate markers exits 0 with pass:false, counts 0/1/0, and one warn/gate/not-gated notice using the established JSON keys and types. The notice is unjudged scope, not a registry check or a warning to promote; strict settings do not change it. The library and gated verdicts retain their Boolean/count relationship. Rejected: approving unjudged scope, changing Boolean types or adding keys, promoting the scope notice, weakening the library, skipping unknown-check validation. |
| 0154 | Unknown staged scope cannot clear the map rule. Supersedes the tested green-research/unknown-list allowance: unreadable scope is an ordinary architecture-map block even under fail-open. The default commit CLI acquires one actual complete successful bounded NUL Git diff after overrides; the hook delegates instead of checking a probe and trusting a second pipeline. Known empty lists, explicit arrays/null, existing staged options and edit ordering retain their meaning. No new flag, format or atomic index/list promise. Rejected: the old allowance, a checked probe with an unchecked second read, tracked-list fallback, growing argv and an internal-error classification. |
| 0155 | An explicit refusal of this machine's network judges a name asked for by what it resolves to. Refines ADR-0110/0114/0118/0125: the guard's exemption for the host and port asked for covers only the literal-address judgment, so a name pointed inward by a resolver is refused even when it is the page's own; under allowInternalRedirects: false the fetch child and the guard child judge a name asked for before fetching or launching (EINTERNAL, reported as the refusal). A literal address asked for stays the caller's, deliberately; the default policy is unchanged, and whether direct internal access should need the opt-in is left open with a named trigger. |
| 0156 | Direct internal access keeps the operator's-statement default. Takes the policy choice ADR-0155 left open: with no refusal set, a planned or searched URL that is itself internal stays the operator's statement and is fetched (ADR-0110); refusal remains the collector's false or the variable set to 0, which ADR-0155 makes cover names from the first URL. A dated default with ADR-0155's trigger kept for reopening. Rejected: refusing every internal first URL without the opt-in, rejecting internal hosts in the plan parser, leaving the question unrecorded. |
| 0157 | Feature and ranking work stays held until the matched comparison reports. The PR #266 queue's conditional feature and ranking items remain held under ADR-0117 until the ADR-0145 comparison has run and its per-mechanism ADRs exist; no freeze exception is granted. Rejected: proceeding on a green branch, a blanket exception for the set, dropping the items outright. |
| 0158 | A small-core extraction is a proposal, not a plan. The 2026-10-10 audit's core boundary sketch (93 files, 25,565 physical lines at that head; release-evidence validation, the panel and optional surfaces as candidates for separate distribution) stays a proposal until the ADR-0145 results exist and a measured dependency review of the seam is recorded under docs/decisions/ (ADR-0030). No boundary, command, distribution or freeze exception adopted. Rejected: extracting the release layer now, rejecting the proposal, keeping it only in working notes. |
| 0159 | The 22 line starts at 22.22.1, because 22.13.0 to 22.22.0 lose windows-1252. Those builds' TextDecoder reads windows-1252 as Latin-1 (nodejs/node#60888), so a page's euro signs and curly quotes are captured as control characters with no warning; nodeLine reports them as fail naming the defect and the first good build, the README says 22.22.1+ or 24+, the hard floor every command refuses below stays at 22. Rejected: a table decoder for 0x80-0x9F behind a runtime probe, refusing those builds in checkNode, documenting only. |
| 0160 | Claimed package approval is re-derived from its checked corpus with an unpromoted portable gate; false claims remain false, and major-1 early stamps require their actual historical input hash and a missing gate field. |
| 0161 | A WARC export names the resource block's own hash (body-sha256) beside the whole capture file's, leaves out a ledger entry it cannot date (WARC-Date is mandatory) instead of writing an empty one, exits 2 (INCOMPLETE) on a partial export, and re-reads the written file (rank 7, 2026-10-03 gap audit; rank 15, 2026-10-04 gap audit) |
| 0162 | A [quote: ...] marker is read at any bracket depth (a two-level passage was not a marker at all, so it went unchecked), its fragments must lie in one passage (MAX_QUOTE_GAP, tried at each occurrence), and the short-quote floor is measured per fragment rather than over the joined text (rank 4, 2026-10-03 gap audit). Amended 2026-10-19 (review of the 2026-10-19 sweep): in that floor a character of a script without word spaces weighs a third of a word, as in ADR-0176, so an elided Japanese or Chinese quote of full sentences is checked again instead of warning quote-too-short, and a fragment is short only when it is short on spaces too, so a spaced 每月 20 美元 is still checked (second review of the 2026-10-19 sweep, F1); rejected: the joined measure for such scripts, splitting on punctuation, weighing each spaced run as at least a word |
| 0163 | A shingle is a word in any script: \p{L}\p{N}\p{M} and NFC replace the Latin-ASCII [^a-z0-9] class, so a page in Russian, Arabic, Hebrew, Greek or accented Latin is fingerprinted instead of shingling to nothing - which made a republished copy of it report as an independent witness, the exact inversion this module exists to prevent (rank 17, 2026-10-04 gap audit; rank 14, 2026-10-03). ASCII input is byte-identical, so MIRROR_THRESHOLD and its measurement still hold. Rejected: character n-grams now, per-script segmentation, NFKC, transliterating. Superseded in part by ADR-0176: in a script without word spaces a character is a token. |
| 0172 | The MCP stdio loop frames synchronously and answers pings while a call is in flight (supersedes ADR-0034's one-chain consequence). Each data event cuts, caps and parses its chunk with no await, so the 4 MiB cap holds during a call (#346); tools/call keeps its own chain, one at a time in order, and ping, tools/list and the rest go on a second chain that never waits for a call (#347). Rejected: every answer concurrent, pause()/resume() back-pressure, unordered non-call answers. |
| 0173 | A topic request waits for an approved topics folder. With no folder approved, auto-collect reports a topic request waiting with the reason - no result, no commit - and runs it unchanged on the first cycle after the operator sets the folder, as it already does while paused or over today's cap (#323). Amends ADR-0148 decision 4's last sentence; content and target-safety refusals are unchanged. Rejected: keeping the final refusal, refusing then re-queuing on a settings change, retrying every refusal. |
| 0174 | A page the kit refused for what it is is not fetched again within the refresh window (extends ADR-0100). An 11 MB page, a PDF and a 403 were scraped and paid for on every run; recentlyGone now also remembers the size cap and the binary refusal as refused on any route, a standing 4xx (not 405/407/408/429, never a rate limit) for the route that got it, and reads a 404/410 in the transports' wording too. 5xx, timeouts, rate limits and transport errors are still retried; --force retries anything. Rejected: remembering every failure, leaving the 4xx out, remembering 408/429 or a proxy's 405/407, a structured ledger field, keying all or none of the memory on the transport. |
| 0175 | A quote marker the gate cannot read blocks (citations/quote-unreadable, fail): a [quote with no closing bracket, a space before the colon or no colon was not a marker at all, so an invented sentence in that shape passed where the same sentence written [quote: ...] failed; measure counts it as a quote not found, not as an unanchored row (#319). Not attempts: inside a read marker, a code span, a URL, or Markdown link text. 0 hits over the repository's 41 projects. Rejected: a tolerant parser that guesses the passage, warn-only, counting it as unanchored. |
| 0176 | A shingle in a script without word spaces is fifteen characters (supersedes in part ADR-0163, refines ADR-0036). In Chinese, Japanese, Thai, Lao, Khmer and Burmese a token is one character with its marks, and a shingle is the shortest run weighing five words, a character weighing a third (CHARS_PER_WORD = 3), so a Japanese page and its republished copy are one document, not two witnesses (#333); MIN_SHINGLES is counted in words of text. Measured on Chromium's, vim's and Python's own Chinese, Japanese and Thai text: the plain character 5-gram put two different Thai texts at 0.18 and two translations of one UI at 0.24, fifteen characters at 0.02 and 0.06, with two versions of one text at 0.57 to 0.68 (English 0.65). Every other script shingles as before; no verdict moves over the repository's 417 captures. Rejected: plain character 5-grams, a length per script, dictionary segmentation, character n-grams for every script, Hangul by character, a basis chosen per page, a floor counted in shingles. |
| 0180 | config-ill-typed covers every typed config key. A transport, searchTransport, serpapiKey, searxngUrl, browserPath, projectSkillDir, maxAgeDays, editGate.settingsPath or skillRoots that is present and not of its type keeps its default and is named, with what it should hold; a skillRoots list that loses an entry is named too; an editGate that is not an object is editGate.mode ill-typed, resolved to hard-block (#310). Extends ADR-0121, which named the posture keys only; rejected: posture keys only, a second finding name, an unreadable config on a wrong type, ask for a non-object editGate. |
| 0182 | The gates judge a path by the project it belongs to in its repository (#309). git runs the commit hook at the top level, so a project below it was never judged; at an ungated top level each staged path now goes to its nearest project below - a folder whose tracked research/DISCOVERY.md new-project rendered, or that holds research/raw/, so data folders and the vendored template are not projects and a clone that never received raw/ still is - judged from the index and named in the verdict. The edit gate judges a named file the same way, asks a gated top level alone, and keeps the raw/ and GATE_OFF refusals for every project above the file; doctor says where the commit gate runs. Rejected: judging by the cwd, any one of the four markers below the top, DISCOVERY.md or kit.json alone, raw/ on disk as the only second signal, disk-only markers, an unbounded walk up, judging nested projects under a gated root (deferred), a doctor warning alone. |
| 0183 | The suite rule runs on the commit, not the working tree (supersedes in part ADR-0120; fires ADR-0024's deferred trigger; amended 2026-10-19 by the review of PR #430: the skip-worktree entries absent from the working tree (a sparse checkout's) stay skipped in the scratch while a file the user hid and has on disk is checked out (its check (c)), LFS pointers are not smudged there, and the block quotes git's fatal: or error: line, not its first). The commit gate ran the checkout's runner in the working tree, so an unstaged red edit blocked a green commit and a red file staged then fixed on disk was allowed. The suite now runs in a scratch repository built from the index being committed (a partial commit's GIT_INDEX_FILE included): ls-files --stage read in the hook's environment, git init under the temp folder with the real object store as an alternate, update-index --index-info, checkout-index with core.longpaths, HEAD at the real HEAD, the commit's object format, the long temp name; a scratch that cannot be built blocks with git's error, as do an unmerged index and an index changed during the run; a killed gate's scratch is swept after a day; intent-to-add entries are left out, as the commit leaves them; a folder without .git is judged as it is. About 10 s on the 1743-file tree. Rejected: materialising fetchproof/ only or a copy without .git (repo-hygiene skips silently), write-tree (may write the commit's index), stash, refusing on divergence, passing GIT_INDEX_FILE to the suite, git worktree add, falling back to the working tree, removing the scratch on a signal (spawnSync blocks the handler). |
| 0184 | A commit that removes HEAD's evidence is judged, unless its project goes whole (extends #312 and ADR-0182; review of the 2026-10-19 sweep, hooks F1; amended 2026-10-20 by check (c), F2: the top level's evidence moved whole below it - one other folder's staged ledger holding every entry of HEAD's, every capture HEAD holds beside it as the same blob, and no path left under the top level's research/ in the index (check (e), E2) - is a move, not judged where it was; with the old research/ left on disk holding only untracked files it blocks, naming them, until it goes (check (e), E1)). #312's integrity block ran only for a gated project with a corpus in the index, so rm -rf research && git add -A left no marker and committed silently, git rm -r research with the gate's ignored log left was told a git add research/ that exits 128, and a nested project's research/ removed while its code stayed was allowed. headEvidenceRemoval now asks what the commit takes from HEAD's ledger and captures wherever the gate answered without asking - no marker on disk, no corpus in the index, and the ungated top level in bin/gate.mjs - and blocks with #312's reason and git checkout HEAD -- fix; research/GATE_OFF stays the recorded override. A project below the top whose folder keeps no tracked path (deleted or moved whole, one literal git ls-files) is not judged where it was; the top level always stays. Rejected: A, block every removal (a move gets a false "restore it", the override lives in the folder being deleted), A' (A plus naming that GATE_OFF), C (a message rewrite that leaves the silent path open). |
| 0185 | A render's folder is made by the transport and holds the browser's profile (break-test B1). Chromium ran without --user-data-dir and left a HeadlessChrome* profile in the temp folder per render (1841 folders, 656 MB, on the operator's PC), and a guard child killed at guardChildTimeout left its fp-netlog-* folder. renderGuarded now makes one fp-render- folder under tempBase() per render, passes it to the guard child as the job's folder, and removes it after spawnSync returns, however the child ended; the child gives the browser --user-data-dir=<folder>/profile and --log-net-log=<folder>/net-log.json and removes the folder in its own finally. removeRetrying (core) retries a held removal (Windows EPERM/EBUSY/EACCES, ENOTEMPTY anywhere; 25 ms doubling, about 1.6 s), then leaves the folder and never throws, so cleanup never changes a result. Rejected: the child reporting its paths (spawnSync reads nothing until the child is gone; a side file is a leftover itself), sweeping the shared temp folder by name, rmSync's maxRetries, pointing the browser's TEMP into the folder, killing the browser's tree (a separate change). Superseded in part by ADR-0188: a killed browser is not waited for past 3 s, and a folder it still holds is tried again by the transport. |
| 0186 | The kit is renamed Fetchproof, with a clean break (the owner, 2026-10-20). research-kit/ is fetchproof/, ~/.agents/research-kit is ~/.agents/fetchproof, RESEARCH_KIT_* is FETCHPROOF_*, and the package format, accounting packet, MCP server name, User-Agent and collector artifacts follow, and the initials (RK- package IDs, rk- temporary folders) become FP-/fp-; dated records are renamed too. Never renamed: corpus raw/ files and any other file the ledger hashed, the received archive's planning documents (ADR-0028), a corpus's URLs and quotes, a pinned link's path, other sites' URLs, the names other people gave things (Apple's ResearchKit, a recorded vendor result, SkillsMDs' references/research-kit.md), plan inputs paired with pages fetched under the old name. No old name is read as a fallback. Rejected: keeping the old names working, leaving dated records as written, renaming only the display name, renaming evidence. Lifts the ADR-0117 freeze for this item. Superseded in part by ADR-0189: the received archive's planning documents are renamed too. |
| 0187 | The handoff checks integrity, not currency, and shows the snapshot's age (discussion #413; lifts the freeze, ADR-0117, for this one item). A builder got handoff OK and PASS for a corpus whose source page had changed: correct, since handoff checks the corpus arrived intact and a builder cannot fetch, but nothing showed how old the snapshot was. verifyHandoff returns fetched: { from, to, oldestDays } over every evidence row a later capture has not superseded, each dated as freshness dates it (freshnessDay: the retrieved of the capture the Raw cell names, the Retrieved cell when it names none). The OK line adds captures fetched <from>..<to> (oldest N days) on every role. The age is never a finding and never changes the exit code. Rejected: docs only, a Retrieved column in the brief (C2: a format and body-hash change for the same fact), the git commit in the brief (C3: a brief cannot name its own commit, and the inputs hash pins the corpus), a handoff threshold (stale-evidence owns it), dating by the editable cell (G8), a second dating rule for a blank Raw cell, counting superseded history (it kept a re-checked page's first age), a builder-only line. |
| 0188 | A killed browser is not waited for, and its render's folder is tried again (the flaky LIVE /stalled test, 2026-10-21; supersedes ADR-0185 in part). On the operator's Windows PC Chromium printed the whole page, did not exit, and took 15 s to more than 35 s to go after the guard's kill; the guard child waited for that exit past the 10 s the parent allows it (ADR-0119), so the parent gave it up and the page with it. renderThroughGuard now waits KILL_GRACE_MS (3 s, armed before the kill) and then reports the page read; when it never saw the exit it tries the folder once (removeRetrying(dir, { retries: 0 })). renderGuarded keeps, for its own process, the folders it made and could not remove, and tries each once, with no wait, before every later render and at exit. startGuard's close destroys every socket it accepted: a handed-over CONNECT the browser kept open kept server.close waiting. The launch allowance runs from the guard child's own start (since: 0, on its monotonic clock), superseding ADR-0119 in part: one loaded run still lost its page with every step after the kill bounded, and the child's boot was in none of the parent's times. Rejected: widening the test's timeout, raising guardChildTimeout, a shorter grace, the parent waiting up to 20 s for a held folder, a sweep by name (ADR-0185), the tree kill ADR-0185's fired trigger named (measured while the hang reproduced: the browser's exit came 44.6 to 53.3 s after child.kill() and 13.3 to 51.2 s after taskkill /T /F, neither within the grace). Trigger: a killed browser's exit comes within the grace again, or a tree kill measured again ends it within the grace where child.kill() does not, or fp-render-* folders keep accumulating. |
| 0189 | The archive's planning documents are renamed too, and their digests are rewritten once (the owner, 2026-10-21: "Rename them too"; supersedes in part ADR-0028's "never updated" and ADR-0186 item 3's archive bullet; docs only). The 44 received planning documents that spelled the old name (RESEARCH_REPORT.md, 15 of ADRs 0001-0021, the 2026-09-14..16 reviews, 5 plans, 18 specs) take ADR-0186's spelling map: 319 of 320 mentions; e7f4d76:research-kit/lib/finding.mjs is kept, a path in a commit's tree. Their 44 BUNDLE_INDEX.md rows hold the renamed digests, each old one checked against main first; the received digests stay in git history (git show 7470287:BUNDLE_INDEX.md). The other 36 rows, EXPECTED_TO_DRIFT and the code are unchanged, and the inventory is still never regenerated. Rejected: leaving them as received (our recommendation: a frozen digest answers "what did we receive?"), exempting them in EXPECTED_TO_DRIFT, keeping both digests, regenerating the inventory, renaming the pinned path, rewording bundle.mjs's summary now. Trigger: history before 2026-10-22 rewritten or lost, another rename, or a reader misled by "byte-identical to the 2026-09-17 archive". |
| 0190 | The kit vendors lead-orchestrator 2.0, and break-test's parallel-shell lesson (the owner, 2026-10-22; amends ADR-0146 decision 3; skill text only). The vendored 1.x (SkillsMDs 3f2d2fc, file leadorchastratorv2.md) had no run ledger, yet the vendored auto-build runs lead-orchestrator's Phase R, keeps its stage table in RUN.md and writes the PR body from references/report-templates.md. The owner's claude.ai account copy of 2.0 (2026-10-04) ships with ADR-0186's rename and nothing else changed: SKILL.md and six references, without its own integration reference; the kit's references/fetchproof.md stays and governs, and says the project's conventions come first. auto-build's ledger pointer names references/run-ledger.md again. break-test's section 0 item 4 and probe-recipes.md take the account copy's lesson that parallel shell calls share one working directory. Provenance by sha256 of the 2026-10-22 backup; REUSE.toml and NOTICE updated. Rejected: shipping 2.0's integration reference (in place of the kit note, or beside it), keeping 1.x, rewriting 2.0 in the kit's vocabulary, leaving auto-build's pointer, waiting for SkillsMDs. Trigger: SkillsMDs publishes 2.0 or later, the owner's copy moves past 2.0, an agent acts on a kit-note/skill disagreement, or the owner prefers 2.0's integration reference. |
Domain vocabulary lives in CONTEXT.md.