What it answers: why it is like this. · Who reads it: whoever doubts a decision, deliberately — not every session. · How it is pruned: it is not.
Appended, never rewritten. A decision that changes is not edited: a new one is written that revises it, and the old one is marked with a pointer to it. Knowing something was decided, and why it changed, is worth more than the final version alone.
The why lives here; the current state lives in
SESSION.md, if the project has one. 🔴 Deliberately not linked: that file is not created with this one — it is offered separately and the project may not want it — so a link would be born broken.
- Status: ✅ accepted
- Date: 2026-08-04
- Decision: The harness's layer 1 is a numbered list of invariant rules, not
a table and not a checklist. A rule enters only if it answers three questions:
is it true and does it bring its evidence? Would an agent get this wrong
without the line? And does the harness not already write the file the rule
asks for? A rule that a file makes unnecessary does not qualify. Rules cite
both number and text; numbers shift when rules leave, the text survives.
Cross-cutting invariants — such as "done means verified by CI" — live in layer
1; stack-specific details live in layer 2 modules. A rule that delegates to
another place is checked against that place. The layer is edited down, not
grown:
layer1.mdcarries 18 rules. - Reason: Layer 1 is copied into every project. Every line there competes for context window and attention. A bloated layer 1 is ignored; an exhaustive one is worse than a short one because it hides what matters. Tables break formatting for long sentences. Rules that only describe the template duplicate its structure. Rules without evidence are opinions. Rules that delegate without checking their destination become false promises.
- Rejected: tables for rules; copying template structure into layer 1; growing layer 1 indefinitely; rules that state data visible in the repo; stack-specific CI details in layer 1; claiming "done" without CI.
- Status: ✅ accepted
- Date: 2026-08-04
- Decision:
.envfiles, credentials, keys and real data dumps are not read without explicit permission, and they are never committed. Only.env.example— a template with no secrets — travels in the repository. - Reason: What an AI reads enters the conversation and from there can leave the machine. A credential committed is a credential published. Permission to read real data is a human decision, not a default, and the safe file to share is the example.
- Rejected: reading
.envto "understand the configuration"; committing dumps for debugging; treating credentials as just another source file.
- Status: ✅ accepted
- Date: 2026-08-04
- Decision: A harnessed project keeps volatile state in
docs/SESSION.md, written when a task closes and overwritten each time. Reasoning lives indocs/DECISIONS.md, append-only, with old decisions marked rather than deleted. A singlememory.mdor amemory/folder is not used.SESSION.mdwarns when it grows too large, but the warning is a trigger to review, not a hard limit. - Reason: Measured on existing projects, the file that tried to do both
state and reasoning either swelled to hundreds of lines or was abandoned at
three lines. Separating them lets each file do one job: one is overwritten
because git keeps the history, the other accumulates because the reasoning is
part of the record. A hard size limit on
SESSION.mdwas tried and failed; the useful part was the act of looking, not the number. - Rejected: a single
memory.md; amemory/folder; hard size limits onSESSION.md.
- Status: ✅ accepted
- Date: 2026-08-04
- Decision: The harness's subagents (
architect,reviewer,security,tester) are generic roles, not project-specific staff. They are deployed bysync:globalto the user's global configuration directories (~/.claude/,~/.config/opencode/agents/), never copied into individual repositories. Each project may point to them, but it does not own or edit them. - Reason: Roles are shared patterns; copying them per repo guarantees drift
and multi-machine rot. A single global source, versioned with the harness, is
the only place a fix or a new role has to land. Project-specific behaviour
belongs in
AGENTS.md, not in a private copy of an agent. - Rejected: per-project agent files; editing global agents inside a repo; duplicating the same reviewer across repositories.
- Status: ✅ accepted
- Date: 2026-08-04
- Decision: Each harnessed project keeps its own
skills-lock.json. It is not shared with other repositories, not moved to a global location and not treated as a reusable asset. - Reason: The file records the exact SHA-256 hashes of the skills installed in that repository. Sharing it between projects would make the hash list a lie the moment the projects diverge. Global skills are fine for generic agents; project-specific locks are fine for reproducible project state.
- Rejected: a global
skills-lock.json; copying the lock between repos; ignoring it from version control.
- Status: ✅ accepted
- Date: 2026-08-04
- Decision: Layer 1 has no fixed size cap: a new rule must still justify
replacing an old one. The full
AGENTS.mdhas no enforced maximum either.docs/SESSION.mdat 200 lines is a signal to check for redundancy, not a prohibition. - Reason: Hard caps are either ignored or gamed. A 110-line limit on layer 1
in particular would silently allow the rest of
AGENTS.mdto bloat. Editorial discipline — every rule earns its place, every long file is flagged — works better than a number that passes or fails a build. - Rejected: a 110-line cap for layer 1; a 500-line cap for the full
AGENTS.md; treating line counts as CI failures.
- Status: ✅ accepted
- Date: 2026-08-05
- Decision: Commit messages are written in English and follow
Conventional Commits:
type(scope): summary, with!for a breaking change. The body carries only the list of changes and the reason for each one. The types are the standard eleven:feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert. - Reason: English is the language of identifiers and tooling; mixing Spanish commits with English code creates a bilingual history that is harder to scan. Conventional Commits is already understood by release tools and changelog generators, and limiting the body to changes and reasons keeps history readable without inventing a custom format.
- Rejected: Spanish commit messages; a custom
[Intent]format; bodies that explain how the change was made instead of why.
- Status: ✅ accepted
- Date: 2026-08-05
- Decision: In a harnessed project,
skills-lock.jsonis committed because it is the only file that makes three machines rebuild the same skills..claude/and.agents/are git-ignored and rebuilt likenode_modules. The harness itself is versioned throughpackage.json, the install seal and git. Any number written in documentation — line counts, commit sizes, park-wide counts — must come from a command (wc -l,git show --numstat, a sweep script), not from memory, and must declare its scope so it can be reproduced. - Reason: If a file does not travel, it does not exist on another machine. Committing regenerated directories creates duplicates and drift; committing the lock does not. And a number in prose looks verified simply because it is a number: the only way to keep it honest is to generate it with a tool every time.
- Rejected: versioning
.claude/and.agents/; writing "several" or "most" instead of counts; quoting figures without saying which command produced them.
- Status: ✅ accepted
- Revised by: ADR-033 — a second workflow runs the checks that time alone
can redden. The CI workflow still invokes
verifyand nothing else. - Date: 2026-08-08
- Decision: The CI workflow invokes
pnpm run verifyand nothing else. Whatverifycontains lives inpackage.json, so adding a check never edits the CI file. Every check that must be green entersverify, is run before it is added, and any check that is red is either fixed first or declared as a written exception with its reason. Time is not a reason to leave a check out. - Reason: Two lists of the same thing in two files drift apart — that is how tests stop running in CI. A CI that is red from day one is ignored from day one, so every new check must be proven green locally before it protects the repo. A written exception is the only way to tell "not yet fixed" from "forgotten".
- Rejected: enumerating checks in the CI workflow; adding a check to
verifywithout running it first; leavingbuildor other slow checks out ofverifyto save time.
- Status: ✅ accepted
- Date: 2026-08-09
- Decision:
init-projectandpropagaterefuse to run on the harness's own source. They detect the source repo by comparing the resolved path, not by inspecting contents, so the guard works on all three operating systems. Anything the harness copies into another repo — templates, layer 1, reference — carries only the mechanism the reader needs; stories about how the rule was discovered, and names of private repos, stay in the private planning record. - Reason: A script that can injure its own source turns a mistaken invocation into a silent self-sync loop. Path comparison fails across Windows case differences and Unix symlinks, so the guard must ask the filesystem. And a CVE from another project or an instruction like "when copying this, check…" is useless or harmful in the destination: the reader is no longer copying, and the private repo may be renamed or deleted.
- Rejected: comparing resolved paths; allowing the script to run on its source with a warning; copying anecdotes and private repo names into public artefacts.
- Status: ✅ accepted
- Date: 2026-08-10
- Decision: The harness ships
reference/traps.mdwith failures the project has already paid for, each one naming the symptom, the cause and the remedy. A trap is only kept if it names its remedy; a failure without a fix is noise. The private census of which projects hit which trap stays outside the public harness; what travels is the pattern and how to check it. - Reason: Agents repeat the same expensive mistakes because they do not inherit project memory. A trap that says both what can go wrong and how to verify the fix turns a private lesson into a reusable guard. The census of where it happened is private data; the pattern belongs to every project.
- Rejected: keeping traps without remedies; publishing the per-project census; relying on agents to remember past sessions.
- Status: ✅ accepted
- Date: 2026-08-12
- Decision: Documentation and CI use
pnpm run verify, never the short formpnpm verify. The script inpackage.jsonis the single source of truth for what runs in CI and locally. - Reason: The short form can silently behave differently across platforms or
pnpm versions — on Windows it once produced a false green by resolving to
something other than the
verifyscript.pnpm run verifyis unambiguous: it always runs the script declared inpackage.json. - Rejected: relying on
pnpm verifyin docs, scripts or CI.
- Status: ✅ accepted
- Date: 2026-08-16
- Decision: A repository receives one harness, at its root. Layer 2
declares the union of whatever stacks the detector finds, but no harness files
live in subfolders. What the root cannot declare — different Node majors for
different folders, separate
verifyscripts per folder — is declared as an exception in layer 2 and written by a person, not inferred by a mechanism. - Reason: The detector already scans the root and two levels down, and the
layer-2 mark already admits several stack modules. The alternative — one
harness per folder — would duplicate the layer 1 in every subproject and
create nested
CLAUDE.mdfiles that Claude Code loads on demand and does not reinject after/compact, so the harness silently disappears mid-session. The root harness is the one that survives. - Rejected: one harness per project folder; making
propagatewalk subdirectories; using.claude/ruleswithpaths:.
- Status: ✅ accepted
- Date: 2026-08-29
- Decision: A harnessed file may contain at most one block per mark
family. The parser recognises both the old Spanish keys (
capa1,capa2) and the new English ones (layer1,layer2), but it always writes English. A file whose marks do not pair, or that contains two blocks of the same family, is refused — nothing is written and the exit is non-zero. The retiredrules/reglasandrepairs/reparacionesfamilies are not taught to the scanner as single-comment blocks because those blocks were never updated by any command. A grafted block is separated from the project prose by exactly one blank line. - Reason: The marks are the only boundary between harness-owned and project-owned text. If the parser reads them in more than one way it can silently replace project prose, as happened when an unclosed opener paired with a later injected closer. Two blocks of the same family leave the file with two contradictory versions of the same layer. Refusing keeps the damage where a human can see it, and the English-only write avoids carrying the private repo's name inside every public mark.
- Rejected: guessing where a missing closer belongs; updating both blocks of
a duplicated family; teaching the scanner the single-comment
rulesform; leaving bilingual marks in newly written files.
- Status: ✅ accepted
- Date: 2026-08-06
- Decision: A command exists only if it does something that a rule plus a
sentence cannot. The three valid reasons are: it restricts permissions (a
read-only subagent), it imposes an order that would be forgotten (tests before
committing), or it fixes an output format that makes two runs comparable.
Commands that merely apply a rule already loaded in every session are removed.
The harness started with eleven commands and was reduced to six:
before-commit,commit-all,find-dead-code,init-project,propagate, andwrite-docs. - Reason: The stronger layer 1 is, the fewer commands are needed. Every command is generated in two dialects, so each surplus is paid twice. A command nobody invokes because it is faster to ask for the same thing in prose is dead weight that also drifts out of sync with the rules.
- Rejected: keeping commands "just in case"; replacing rules with commands; growing the command set as the rule set grows.
- Status: ✅ accepted
- Date: 2026-09-05
- Decision: Layer 1 does not include a default list of skills. When a
project starts, the owner runs
npx autoskills --dry-runin their own terminal, reviews the proposals, and decides what to install. The harness ships no skill catalogue of its own. - Reason: Skills that are genuinely stack-independent and stable already
live at the global level and are distributed by
sync-global. A per-project list would either be empty or would invent cross-cutting recommendations without evidence. The filter is human because the detector gets the language right and the role wrong. - Rejected: shipping a default skill list; formalising an empty list to
declare a blind spot; installing skills automatically during
init-project.
- Status: ✅ accepted
- Date: 2026-09-05
- Decision: A harness command is not executed in tests to prove it works;
what is tested is what it leaves written. Each deterministic effect has
its own test — link checker for links, marks guard for marks, format check for
formatting, ASVS tests for the generated indexes. The only thing tied directly
to a command is the ADR template shape that
/write-docsdictates. The/propagatecommand exists because a graft with no owner would leave layer 1 unmanaged; it is the counterpart to/init-projectfor files that already exist. - Reason: Commands mix measurable text with judgement. Testing the judgement
would measure something else and call it obedience. Testing only the effects
keeps the tests deterministic and keeps the command's prose free to explain
without becoming a specification.
/propagatecloses the loop: layer 1 must be updatable without re-running the fullinit-projectflow. - Rejected: running commands inside
verifyand inspecting their output; a general framework for command verification; testing command prompts as if they were scripts; leaving grafted layer 1 without a dedicated command.
- Status: ✅ accepted
- Date: 2026-09-07
- Decision: When harnessing a project,
init-projectchecks whether the repo has the basics layer 1 demands and reports what is missing. The checks are:.gitattributes, a pinned Node version (.nvmrcor a non-rangeengines.node), a pinned package manager (packageManager), averifyscript built from real candidates, a CI workflow that invokesverify, and no unchecked.envthat CI will lack. The command then offers to add what is missing —.gitattributesand the CI mould can be written directly;.nvmrcandpackageManagerare asked;verifyis only proposed as a set of candidates to run, never written blindly. Nothing is installed without the owner's say-so in that exchange; what is not authorised goes todocs/SESSION.md. - Reason: The harness was handing out rules and never saying when a repo did
not meet them. A
verifyborn red is ignored from day one, and a CI written before the version and package manager are pinned is red on arrival. Offering the fixes one block at a time, with the cost of each visible, turns the report into action without removing the owner's decision. - Rejected: only reporting and leaving the owner to remember; writing
foundations silently; proposing a
verifyscript without running the candidates first; deducing.nvmrcfromengines.nodeor vice versa.
- Status: ✅ accepted
- Date: 2026-09-07
- Decision: The harness ships an
uninstallcommand with two scopes. For a project, it removes the harness blocks fromAGENTS.mdand any mould file that carries a block, but it never deletes the file itself and it deletesCLAUDE.mdonly when it is the bare pointer the harness wrote. For a global deployment,--global --dest <folder>removes exactly the files tracked by the install seal and nothing else. The command is dry-run by default and lists what would disappear. - Reason: Installing something you do not know how to remove turns a trial
into a commitment. The marks make removal simple:
outside()already proves thatpropagatenever touches anything beyond them. Without a record of what was deployed, the global uninstaller cannot tell ours from someone else's, so it refuses to act. - Rejected: removing whole files by default; classifying remaining files as "skeleton" vs "owned"; deleting without a dry-run first.
- Status: ✅ accepted
- Date: 2026-09-14
- Decision: A harnessed file has exactly two layers:
- Layer 1 — inside the harness marks, written and updated by the harness.
- Layer 2 — everything outside the marks, owned by the project. The harness writes layer 1 at the top of the file so rules are read before the prose they govern. When a file already exists, everything outside the marks stays byte-for-byte identical. The generator never writes layer 2; an agent may propose layer-2 sections, but only with the owner's approval.
- Reason: The mark is the only reliable boundary between harness-owned and project-owned text. Anything the harness writes outside its marks becomes project text the moment it is written and is never touched again. Keeping the boundary physical prevents the harness from silently overwriting project prose.
- Rejected: three layers; a layer 2 of stack-specific rules written by the harness; scaffolding or placeholder sections in new files; writing layer 2 automatically.
- Status: ✅ accepted
- Date: 2026-09-15
- Decision: The global installer writes only into the folders it creates:
~/.claude/{agents,commands,reference}and~/.config/opencode/{agents,commands}. It never writes tool configuration files such as~/.claude.json,~/.claude/settings.jsonor~/.config/opencode/opencode.json; it reads them and reports what is missing.reference/is deployed whole, byte for byte, including CSVs and PDFs, to a single copy at~/.claude/reference/. The installer warns when opencode cannot read that path becausepermission.external_directoryis missing. - Reason: Tool configuration files contain personal settings and
permissions; merging into them is the most dangerous operation the installer
could do, so it is avoided entirely.
reference/needs to be complete and identical on every machine because prompts cite paths inside it; splitting or filtering it would force the dialect converter to rewrite paths inside prompt bodies and would silently drop files such as CSVs and PDFs. - Rejected: writing or merging tool settings; deploying two copies of
reference/; filteringreference/by extension; using a third neutral folder or symlinks.
- Status: ✅ accepted
- Date: 2026-09-16
- Decision: The harness ships an OWASP ASVS index in
reference/security/and asecurityagent that uses it. A harnessed project declares its ASVS level and re-estimate trigger in its## Securitysection; if none is declared, the agent derives the level from three signals (accounts, money/personal data, own backend). The same section may declare anAreas:list mapping project paths to ASVS chapters; paths not in the list are read, deduced and proposed for addition. The harness itself runsasvs-ageinverify, which cuts after 180 days by reading the stamped date inreference/security/source/README.md. Theasvs-updatecommand is manual and never automatically bumps the OWASP tag.pnpm auditbelongs in abefore-commitstep, not inverify, because it fails on upstream CVEs the project cannot fix. - Reason: A security review needs a depth dial that is set before the review
starts, otherwise the agent either misses risks or wastes time on irrelevant
chapters. The level and area list live in project-owned layer 2 because only
the project knows its threat model. The age check lives in the harness repo,
not in every project, because the harness maintains the ASVS sheet; a project
cannot fix a stale standard.
pnpm auditis useful at commit time but blockingverifywith fresh transitive CVEs trains developers to skip the whole verification pipeline. - Rejected: inferring the level and areas from scratch on every review;
running
asvs-updateautomatically inverify; shippingasvs-ageto every harnessed project; puttingpnpm auditinverify.
- Status: ✅ accepted
- Revised by: ADR-035 — the decision stands, its reason was too broad.
Claude Code now reads
AGENTS.mdon its own; what makes the import necessary is theCLAUDE.mdthis harness writes. - Date: 2026-09-17
- Decision: A
CLAUDE.mdis a pointer when it contains the line@AGENTS.md. Content that is specific to Claude Code may stay below that line; content that duplicates rules fromAGENTS.mdmust be migrated. The two problems are never mixed: a missing import is fixed with one line; rules insideCLAUDE.mdare a migration that the owner approves. - Reason:
CLAUDE.mdis read only by Claude Code;AGENTS.mdis the standard every tool reads. Without the@AGENTS.mdline,AGENTS.mdis invisible to the session. With it, Claude Code-specific notes below do not harm anyone. - Rejected: requiring
CLAUDE.mdto hold exactly one line; treating a missing import and rules insideCLAUDE.mdas the same problem.
ADR-024 — Deployment files carry a generated-by mark in the body, and deletion is decided by that mark
- Status: ✅ accepted
- Date: 2026-09-17
- Decision: Every file this harness deploys carries the mark
<!-- generated-by: harness -->in the body, just below the frontmatter. A managed file is deleted only if it carries the mark and no longer exists in the source; a file without the mark is someone else's and is left alone.reference/is never pruned because it contains non-Markdown files that cannot carry the mark. - Reason: The mark answers «did this harness put this file here?» inside the file itself, so the answer survives outside registries or manifests. Putting it in the frontmatter would make opencode forward unknown keys to the model provider; putting it in the body keeps the two dialect bodies identical and keeps the deletion rule simple.
- Rejected: keeping the mark in the frontmatter; relying on an external manifest to decide what to delete; never deleting and leaving retired commands alive on every machine.
ADR-025 — Scripts are not installed on the machine: the install seal points to the clone and --check detects drift
- Status: ✅ accepted
- Date: 2026-09-17
- Decision: The harness does not install executable scripts on the machine.
The global installer writes agents, commands, templates and reference, and
stores an install seal at
~/.claude/harness/install.jsonthat records the version, install time, enabled tools and clone path. The/init-projectand/propagatecommands read that seal to find the scripts in the clone. A separate--checkcompares the deployed copy against the source and fails if they drift; when run without--destit checks the current machine's home. - Reason:
scripts/changes often; a copy installed on the machine is one more thing to keep in sync and one more place for an old version to keep running. The clone already has to exist, so pointing to it costs nothing and avoids the copy. Drift between source and deployed copy is dangerous because the tests read the source while the tool reads the copy; checking it on demand keepsverifyfree of network or machine-specific concerns. - Rejected: installing
scripts/to~/.claude/; installing only a subset of scripts; keeping a separate~/.harness-sync.jsonfile; relying on the seal alone to detect hand-edited deployed files.
- Status: ✅ accepted
- Date: 2026-09-17
- Decision: The harness serves both Claude Code and opencode; it does
not choose one. Commands are generated in two dialects from one source so the
body stays byte-for-byte identical and only the frontmatter differs. The
harness declares where the two tools do not offer the same guarantees:
- In Claude Code, the
bash-allowlist of write commands is translated into scopedBash(...)entries inallowed-tools; in opencode the same list is preserved natively. - In Claude Code, subagents receive
Bashwithout command-level restrictions because that tool removes the whole tool when any pattern is denied; this is declared in each agent's body. - In opencode, the write commands
/init-projectand/propagateinherit the permissions of thebuildagent; this asymmetry is declared rather than hidden. reference/lives only in~/.claude/reference/; opencode reaches it throughpermission.external_directory, and the installer warns when that rule is missing.
- In Claude Code, the
- Reason: Both tools are in real use; picking one would force redoing work when switching back. A shared source with dialect-aware generation costs less than maintaining two copies. Where the permission models differ, the harness writes the guarantee explicitly instead of pretending the tools are identical.
- Rejected: choosing only one tool; duplicating source files per tool; hiding the permission differences; adding a dedicated opencode agent just to scope the two write commands.
- Status: ✅ accepted
- Date: 2026-09-18
- Decision: Official documentation and MCPs are always consulted and never
silenced. If what they say clashes with an ADR, the clash is reported, not
hidden; a verifiable fact (an API change, a deprecation, a CVE) overrides a
preference until the ADR is revised. Rule 17 names Context7 as the MCP
every project uses, with the pattern
resolve-library-idfollowed byquery-docsat the installed version. The installer warns if Context7 is not configured. - Reason: An old decision cannot be allowed to override a new fact, or it becomes a lie that hides security issues and breaking changes. At the same time, the rule must be visible enough that no project "forgets" to consult the docs. Naming Context7 in layer 1 makes the requirement explicit, while keeping the obligation to report when the MCP does not answer.
- Rejected: keeping a curated list of links; naming Context7 only "if available"; saying "the ADR overrides the docs" without distinguishing fact from preference.
- Status: ✅ accepted
- Date: 2026-09-18
- Decision: The harness uses a single Prettier configuration with
printWidth: 80for every file type. There is no override for Markdown; prose and code share the same width. The other keys are the canonical ones:tabWidth: 2,semi: true,singleQuote: true,trailingComma: "all",endOfLine: "lf",proseWrap: "always". - Reason: A single width keeps diffs predictable and avoids the surprise of a line wrapping differently because of its file extension. Eighty columns is narrow enough for side-by-side diffs and wide enough for JavaScript.
- Rejected: per-file-type
printWidthoverrides; a wider code width.
- Status: ✅ accepted
- Date: 2026-09-18
- Decision: The public harness is scoped and written as follows:
- JavaScript/Node — the harness targets JavaScript projects, and does not pretend to be language-neutral.
- English throughout — code, identifiers, configuration keys, mark keys, templates, commands, agents and documentation are all in English.
- Projects write in their own language outside the harness marks. The harness never injects non-English text, neither inside nor outside the marks.
- The boundary is the mark, not the language. What is inside a harness mark belongs to the harness; what is outside belongs to the project.
- Reason: A public harness needs a clear scope and a single language to be copyable and maintainable. Pretending to be language-neutral hides what the tool actually does; allowing bilingual templates would make project prose drift silently between repositories.
- Rejected: a language-neutral harness; translating templates per project; using language as the boundary between harness-owned and project-owned text.
- Status: ✅ accepted
- Date: 2026-09-18
- Decision: The harness pins its toolchain and repository conventions as
follows:
- pnpm only, never npm. Version pinned to 10.34.1 in
packageManager. - Node 24.x in
.nvmrcandengines.node. - pnpm settings —
minimumReleaseAge,saveExact,ignoreScripts, etc. — live inpnpm-workspace.yaml, not in.npmrc..npmrcis reserved for registry and authentication only. .gitattributesis mandatory from the first commit:* text=auto eol=lf, with Windows scripts (*.ps1,*.bat,*.cmd) using CRLF. This keeps line endings deterministic across Win11 and Ubuntu.- The general rule for new projects is: pin the highest LTS that the stack accepts, reading the ceiling from declared dependencies, not inventing it.
- pnpm only, never npm. Version pinned to 10.34.1 in
- Reason: A harness that tells tools what to do is more useful than one that
pretends to be neutral. pnpm 10 is stable, Node 24 is the active LTS, and
pnpm-workspace.yamlis the only place pnpm reliably reads these settings in pnpm 10 and beyond. Keeping them in.npmrchas already silently disabled quarantine in several repos. Mixed line endings are a real cross-platform hazard: a CRLF.nvmrcreads as"24\r"and breaks both CI and the local version switcher. - Rejected: npm; putting pnpm settings in
.npmrcwith kebab-case keys (works today, breaks silently later); pinning a Node number in layer 1 instead of stating the rule; leaving.gitattributesout and relying on editor settings.
- Status: ✅ accepted
- Revised by: ADR-033 — item 6,
asvs-age, leftverifyand runs on a schedule. The other six stay, with their limits unchanged. - Date: 2026-09-18
- Decision: The repo keeps five dedicated guards plus two standard checks in
verify:format:check— Prettier in check mode.leak-guard— scans for absolute paths and private names. The name check only runs where the private-name list is available; in CI it is skipped and reported as a declared exception.marks-guard— detects orphan mark families and old-language marks.vocabulary-guard— catches words an ADR explicitly ruled out.check-links— checks every relative Markdown link and anchor. Absolute URLs and mail links are ignored: their availability depends on the network, and a CI that fails for external reasons is soon ignored.asvs-age— reads the stamped date inreference/security/source/README.mdand cutsverifyafter 180 days.test— the test suite.
- Reason: Each guard watches something a human would miss but that degrades the published repo: leaks, broken links, unmanaged marks, drifting vocabulary, or a stale security standard. The limits are part of the design: a name check without the name list would either publish the names or pass silently, and the 180-day window was derived from OWASP release history.
- Rejected: dropping any of the guards; dropping
templates/CLAUDE.mdinstead, because the code writes the pointer line directly.
- Status: ✅ accepted
- Revised by: ADR-034 — the exception moved out of
LICENSEintoLICENSE-EXCEPTIONS.md. The split of the two licences is unchanged. - Date: 2026-09-18
- Decision: The repository is published under MIT. The exception,
declared inside the
LICENSEfile itself, isreference/security/: it is adapted from the OWASP ASVS and remains under CC BY-SA 4.0. - Reason: The ASVS is © 2008-2025 The OWASP Foundation and is share-alike; relicensing it under MIT would be false. Putting everything under CC BY-SA would impose copyleft on a harness meant to be copied without conditions.
- Rejected: a separate
NOTICEfile — a second file nobody opens does not inform anyone.
- Status: ✅ accepted
- Date: 2026-09-20
- Decision:
asvs:ageleavesverifyand moves, withasvs:check, to a scheduled workflow (.github/workflows/asvs.yml) that runs monthly and on demand. This is the one exception to ADR-009: the CI workflow still invokespnpm run verifyand nothing else, and no second list of checks is enumerated anywhere — the scheduled workflow asks a different question, about the repository rather than about a change. The test for belonging inverifyis whether editing the repo can turn the check green. - Reason:
asvs:agecuts when the stamped check date is over 180 days old, so it reddens because time passed, not because anything changed. In a public repository that lands on a contributor's pull request, for a reason they did not cause and cannot fix: clearing it needs the network and a regenerated sheet, which is the maintainer's work. A red nobody can fix is a red everyone learns to ignore, which is the harm ADR-009 exists to prevent. The rule is already written intemplates/ci.yml, next topnpm audit, and had simply never been applied here. - Consequence, accepted: GitHub disables a scheduled workflow after 60 days
without activity in a public repository. If this one stops, nothing announces
it;
workflow_dispatchand the two commands run locally are the recovery.asvs:checkalso exits 0 when the tag's CSV changed without a version bump, so that case is printed and not signalled. - Rejected: leaving it in
verifyand accepting the red; making it warn instead of cut, which contradicts guards that fail rather than warn; deletingasvs:age, which is the only one of the two that needs no network.
- Status: 🔶 proposed (2026-09-21)
- Decision:
LICENSEcarries the MIT text and nothing else. Thereference/security/exception moves toLICENSE-EXCEPTIONS.md. Which files are under which licence does not change. - Reason: GitHub detects a licence by matching the file against the known
texts. The appended exception dropped the match:
gh repo viewreportedlicenseInfo.key: "other", so the repository showed no MIT badge and did not answer alicense:mitsearch. That is the opposite of what ADR-032 wanted — to state the licence clearly — and the appendix was what cost it. - Rejected: leaving a one-line pointer at the foot of
LICENSE, because it is the same bet that just failed and the match threshold is not ours to know; aNOTICEfile, still, for the reason ADR-032 gave — the name says nothing about what is inside, whileLICENSE-EXCEPTIONS.mdis read from its title.
- Status: ✅ accepted
- Date: 2026-09-21
- Revises: ADR-023 — the decision is unchanged; its reason no longer holds as written.
- Decision:
/init-projectkeeps writing aCLAUDE.mdwhose first line is@AGENTS.md. What changes is what the harness may claim about it. "Without the import,AGENTS.mdis invisible to the session" is only true where aCLAUDE.mdexists — and it exists because this harness wrote it. The precise statement, and the one the prompts must carry, is that aCLAUDE.mdin the working directory or above it suppressesAGENTS.md, and the import is what puts it back. - Reason: Claude Code reads
AGENTS.mdnatively from v2.1.277 through its built-inagents-mdplugin, and its default mode (claude-md-or-agents-md) readsAGENTS.mdonly where noCLAUDE.mdorCLAUDE.local.mdsits in the working directory or above it. Measured 2026-09-21: v2.1.278 installed here, so the native path is live on this machine and the old blanket claim is now false on it. The import is still the right shape for three reasons the native path does not cover: portability — the official docs list five kinds of session that cannot readAGENTS.mddirectly (a version below v2.1.277, a provider that fetches no feature flags such as Bedrock, telemetry disabled, the first session after an install or upgrade, anddisableAllHooks/allowManagedHooksOnly), and the import is what works in all of them; verifiability — anAGENTS.mdread natively is not listed in/memoryor under Memory files in/context, while an imported one is visible through itsCLAUDE.md, which is rule 1 applied to the harness's own loading; hooks —InstructionsLoadedhooks do not fire for a natively readAGENTS.md, and do fire for one reached through the import. The docs say so outright: «ACLAUDE.mdcontaining@AGENTS.md: you can leave it. Keeping the import never makes Claude readAGENTS.mdtwice, whichever Project instructions value you use.» - Rejected: dropping
CLAUDE.mdso the native path takes over — it trades a one-line file for five session types that then read no instructions at all, and costs the/contextcheck that proves the file loaded; a symlink fromCLAUDE.mdtoAGENTS.md, which the same docs rule out on Windows, where git checks it out as a one-line text file unlesscore.symlinksis on — this harness already refuses it incommands/init-project.mdand now cites a source for it; settingclaude-md-and-agents-md, because it is a per-machine setting and a project cannot depend on it (Claude Code ignores it in project and local settings files). - Source: https://code.claude.com/docs/en/memory, read 2026-09-21.
- Status: 🔶 proposed (2026-10-19)
- Decision: When a project's
CLAUDE.mdcarries its own commit-message rule (another language, free format, no Conventional Commits),/init-projectkeeps it as a declared exception to rule 8, with its reason and scope, after asking the owner. Rule 14 keeps its list of what is never an exception (security, AI traces, pushing) and does not grow: the language or format of commit messages is not on it. Nothing inlayer1.mdchanges. - Reason: Two failures were measured on 2026-10-18 and 2026-10-19 with a
small model. One run migrated «always sign with
Co-Authored-By» as an exception to rule 7, which is what the list in rule 14 exists to stop. After that was fixed, another run dropped «commit messages in Spanish» together with it, calling it a contradiction of layer 1 — a legitimate rule of the owner's, lost without asking. Both come from the same missing line between a rule the owner may waive and one they may not, and that line is already rule 14. - Rejected: making rule 8 non-exceptionable in
layer1.md, which would force every project to rewrite its history convention to adopt the harness; adding commit language to the never-exception list, for the same reason; letting the agent drop the rule as «contradicts layer 1».
- Status: 🔶 proposed (2026-10-19)
- Decision:
commands/init-project.mdandcommands/propagate.mdopen with a Step 0 that runsnode -vand, if it fails, stops with a fixed sentence and writes nothing: no later step, nopnpmornpxin its place, no file by hand. A test guards both commands (anode *entry inbash-allow, a Step 0 before Step 1, the check, the stop and the ban on a hand-made substitute).node -vis used rather thancommand -v nodebecause the commands' allow-list hasnode *and notcommand. - Reason: An agent session takes its Node from the terminal that launched
it, not from the project, so a session started with no Node answers «command
not found» and the model improvises. Measured 2026-10-19 with
qwen3.7-plusandPATH=/usr/bin:/bin: without Step 0, none of 3 runs stopped and they changed 4, 1 and 0 files; with it, 3 of 3 stopped with the sentence and changed none. Three runs against three, one model, the command given as an attachment and not as a subtask: it shows the text works, it does not prove it in the TUI. - Rejected: a hook or plugin, which is deterministic but is code outside the harness, differs between Claude Code and OpenCode, and the harness writes one file per project and installs no hooks; a check inside the script, because the script is Node and never runs when there is none.
- Status: 🔶 proposed (2026-10-19)
- Decision: A source may declare
model: fast, a tier and never a model id. Claude Code getsmodel: haiku, an alias that follows the latest Haiku. opencode gets no model — the command runs on the session's, as before — andsync-globalsays so under what does not travel.commit-allis the first, after its text was reinforced: an attribution request loses to rule 7, a secret file is checked by name withgit check-ignoreand never opened, a file the owner asks to change is read before it is added to, the body is a- <file>: <change>, because <reason>list, and no!means noBREAKING CHANGEfooter. - Reason: Measured 2026-10-18 and 2026-10-19 in a throwaway repo with four
logical blocks pending and an unignored
.env, the model read from the API's own record — a model asked which model it is answered wrong both ways. Before the reinforcement: Haiku signed one commit of four withCo-Authored-By, following Claude Code's attribution reminder;glm-5.3-flash-freeread the.envand overwrote.gitignore; both wrote prose bodies. After it: Haiku in auto mode passed 3 of 3, and twice said in its report that the rule beat the reminder;glm-5.3-flash-freepassed 2 of 3, and the third put the README in thefixcommit. A command'smodelapplies in both TUIs, auto mode included, and not inclaude -poropencode run. A model that no longer exists fails withModel unavailablerather than falling back. In opencode, a command's model outside a subtask switches the whole session and stays (amodel-switchedevent; the next command failed on it). - Rejected: a model id in the source, which is one provider's word; for
opencode, a model per machine in an environment variable or a
.env, because the owner works from several machines and a value that lives on each one is a value nobody knows — and a model id is no secret, so a.envwould only hide it; a free opencode model fixed in the repo, since every free one is free "for a limited time" and glm passed 2 of 3, not 3 of 3; picking the first-freemodelopencode modelsoffers, which would swap in a model nobody measured; theuse-haikuandcommitplugins from better-claude, which are code outside the harness (ADR-037). Decision of the owner, 2026-10-19. - Source: https://code.claude.com/docs/en/slash-commands and https://opencode.ai/v2/docs/commands, read 2026-10-19.