Cortex / index
AGENTS.md1 starsChanged 18 days agoMITView on GitHub
Turns a repository into a structural map, then into one ranked report. lib/ holds the logic; the cortex-*.mjs files at the top are the CLIs that skills invoke. Each lib/ module's header comment holds the rules for that module alone — read it before editing the module. - Nothing here may modify a target repository, except by writing under .cortex/. findings returns data; /write-context-docs is the separate skill that applies changes. This separation is what makes "the user decides" structural…
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,843 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository43 other files it gives its agents.
- AGENTS.mdAGENTS.md
- core/AGENTS.mdAGENTS.md
- evals/AGENTS.mdAGENTS.md
- mcp/AGENTS.mdAGENTS.md
- tools/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- architecture-reviewSkill
- audit-notesSkill
- challenge-planSkill
- change-impactSkill
- clarify-requestSkill
- connect-mcpSkill
- create-teamSkill
- daily-planSkill
- debug-issueSkill
- define-termsSkill
- design-specSkill
- end-of-day-summarySkill
- handover-notesSkill
- join-teamSkill
- merge-workSkill
- migrate-legacy-engineSkill
- personal-setupSkill
- quick-noteSkill
- register-projectsSkill
- repo-mapSkill
- resolve-conflictsSkill
- resume-workSkill
- retrospectiveSkill
- review-changeSkill
- set-profileSkill
- setup-repoSkill
- story-mapSkill
- suggest-skillsSkill
- summarize-filesSkill
- sync-websiteSkill
- trim-agent-docsSkill
- update-pluginSkill
- weekly-reviewSkill
- whats-changedSkill
- write-agent-docsSkill
- write-area-briefSkill
- write-context-docsSkill
The file
We split this AGENTS.md into the rules it gives an agent
6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 47
The index is deterministic: No LLM, no network, no clock, no randomness.
- Distinctive: line 127
Do not add a second.
- Very specific: line 140
A path alias is read from the repo, never guessed: Never widen this into inferring an alias from directory names — the value of an edge is …
- Distinctive: line 195
Every target is a listed definition, and its line names it.
- Very specific: line 308
Run on this repo and six public ones: nine rows outside this repo, each opened and each a rule.
- Very specific: line 443
Every CLI but cortex-memory.mjs earns a tools/test/cortex-*.test.sh against a real git fixture, by the rule: does it print a sentence a …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
index/ — the indexer, findings and enrichment
Turns a repository into a structural map, then into one ranked report. lib/ holds the logic; the
cortex-*.mjs files at the top are the CLIs that skills invoke. Each lib/ module's header comment
holds the rules for that module alone — read it before editing the module.
Invariants
- Nothing here may modify a target repository, except by writing under
.cortex/.findingsreturns data;/write-context-docsis the separate skill that applies changes. This separation is what makes "the user decides" structural rather than a promise a model has to keep — if you add a write to a source file here, you have broken the product's central claim. Five named exceptions. The first iscortex-stamps.mjs, in two commands.stampwrites one file/setup-repowas told to stamp, renders and records it in the same act (stampFile), and refuses to write over a file the record does not hold or one edited since, unless--forcecarries the developer's yes.updaterewrites a file Cortex itself stamped, and only in stateupdate— untouched since it was recorded, re-renderable, template changed./setup-reporuns each only for the paths the user confirmed. The rule lives in the code (planUpdates), not in the skill, because a guarantee belongs to the act (ADR 0016). Never widen it toedited,conflictorreview. And never from an older plugin: when the record'scortexis newer than the runningVERSION, its templates are the older ones, every untouched file reads asupdate, and applying it puts the older template back.planUpdatesrefuses the whole plan then, and takesrunningas a required argument so the check cannot be forgotten; status,diffandnext.mjsname it with the two plugin-update commands (olderPlugin).cortex: null(adopted only) and equal versions never warn. The second,cortex-shared-plugin.mjs --write, adds two entries to.claude/settings.jsonon a team's repo (spec S6). It merges and never replaces: it inserts text in the file's own style, leaving every other byte where it was, and keeps an entry already there whatever it says. It refuses a file that does not parse. The parser must agree the result is exactly the original plus the two entries, or nothing is written. Do not re-serialise the file, which reflows every inline array a team wrote.--auto-updateadds"autoUpdate": trueonly on acortexentry this run adds. An entry already there keeps itsautoUpdate— true, false or unset — because the committed value outranks each teammate's own/plugintoggle, and someone chose it. Without the flag the key is never written. The third,cortex-section.mjs --replace, rewrites one section ofCLAUDE.md, and only in stateoutdated(below). Every byte outside the section stays where it was, and the result is read back as exactly one section holding exactly the new text before it is written. Never widen it toedited. The fourth,ensureGitignored(lib/generated.mjs), appends the generated directories a target's.gitignorelacks when a CLI writes under that repo's.cortex/— append only, never.cortex/memory/(it is committed), and nothing at all when--outpoints elsewhere. The fifth,cortex-section.mjs --append, adds a block to the end of a markdown file inside the repo, in that file's own line endings (appendBlock,lib/section.mjs). Every byte already there stays where it was, a block whose heading is already a section is refused, and the path goes throughresolveInRoot. Never let it take a file that is not markdown. - Tests that count loop rows delete
CORTEX_PROFILEat the top of the file, because a developer on a work profile must get CI's answers. Which repo is a team's isteamServed(lib/shared-plugin.mjs). - The index is deterministic. No LLM, no network, no clock, no randomness. Same tree, same
bytes. This is what makes it safe in CI and cheap on every install;
build.test.mjsasserts two runs agree exactly. - A stamped file's state, adoption and the hash rule are
lib/stamps.mjs's header. Where each loop file lands isLOOP_STAMPSinlib/loop.mjs— do not keep a second list. - What the stamp record says is read once, by
stampReport.cortex-stamps.mjs . --jsonprints it andlib/next.mjsranks from it. A new fact about the record goes in the report, never in one of the two readers.TEMPLATES_DIRis exported fromlib/stamps.mjsfor the same reason: four files each resolved the plugin'stemplates/folder. - A command reads the repo once. One that holds a plan prints its last line with
loopLineOfornextLineOf;loopLineandnextLineread the repo, and are for a command that has no plan (cortex-index, andcortex-findingsafter it wrote its report).findings.mjstakescontextLayer, neverreadState, which also plans the loop and asks git.indexedis two facts under one name: in the loop state an index was handed in, inreadStatethe file is on disk. - A section Cortex appends to a shared file (
current·outdated·edited·kept) is defined in the headers oflib/section.mjs,lib/sections.mjsandlib/shipped-sections.mjs. Recomputing the roster from the agents would not do:rosterFor(lib/team.mjs) writes a Cortex-stamped agent as`tester`and an existing one as`tester` (tester), so every stamped section would readedited. - Every CLI here opens through
lib/open.mjs, and declares its flags rather than testing for them — all butcortex-memory.mjs, which takes a subcommand and a--rootthat means.cortex. The declaration is the allowlist: an unregistered or misspelled flag is refused with a message naming it, never reinterpreted as a path. Do not write a ninthparseArgs— there were seven, and each one was a different subset of the same bugs. - Whether a command refuses, degrades or builds when the index is unreadable is
spec.index(openTarget).readIndexalso checksINDEX_VERSION, which for one release was written into every index and read by no consumer at all — so an index in an older format was read confidently rather than refused. - Enrichment is additive. It attaches summaries to files and never edits
index.json, adds files, or removes them. enriched.jsonhas one reader —readEnrichment, defined incore/index-file.jsand re-exported bylib/enrich.mjs— and it returns a state, not a document.okis the only one a caller may trust; the states, their notes and why staleness never points atmergeare in its JSDoc. The viewer declines stale enrichment — the policy and its argument are incortex-view.mjs.findings.mjsandnext.mjsaskhas(ENRICHED_REL)and that is correct — "was this step ever run" is a question about a file existing, andnext.mjsmay only tick a step on the strength of one. Do not route them through the content reader to make the caller count come out at one.- Every reader of
.cortex/memory/here askscore/memory.js, which reads both layouts — a day file<date>.md, and one file per author in a day directory,<date>/<author>.md. That iscortex-memory.mjs recent, the viewer's timeline (readMemoryinlib/overview.mjs, drawn bylib/view-html.mjs), and the memory row ofcortex-nextand the loop state (memoryFilesinlib/loop.mjs, whichlib/next.mjsshares).next.mjsandloop.mjslisted*.mdin the directory themselves, which tells a repo holding only day directories that its memory was never started. The timeline reads the newest three days, and orders a day's entries by time, then no-author before author, then slug: keys read off the files, so the same tree draws the same page. An entry'sauthoris the slug from its path, ornull, and the page shows nothing fornull. cortex-memory.mjs appendpasses no author;core/memory.jsfinds it. The entry lands in<date>/<author>.md. With no usable name it lands in<date>.md, the exit is still 0, stdout is still the onewroteline, and one line on stderr says so on every such write. ACORTEX_AUTHORthat is set and unusable writes nothing and exits 1. A test that runs the CLI setsCORTEX_AUTHORor isolates git (isolatedGitintest/cli.test.mjs), so it never reads the machine's git name.- Validate everything a model produced, but only drop what is actually wrong. Never let an
unreported drop happen — a silently incomplete enrichment looks exactly like a complete one. How
enrich.mjstells a hallucinated path from a renumbered batch is invalidateBatch.
Gotchas
- Vendored is declared in
.gitattributes, never guessed by Cortex. Same rule asgo.modandcomposer.json: declared beats guessed.briefCandidatesandisEnrichableskip it andstats.vendorednames what was skipped. A consumer that ranks or costs by size must use it and say which side it counted: silently dropping half a repo reads exactly like covering it. The one name Cortex adopts is Linguist's own default,vendor/: the walker leaves it out, counts it instats.skipped, and indexes it as source where the repo declares-linguist-vendoredthere (VENDOR_DIR,declaredOwn). Indexing every trackedvendor/as vendored was measured and rejected (#529). The rest islib/vendored.mjs's header. walk.mjsasks git, not.cortexignore. Those answer different questions:.cortexignoresays what is not knowledge in a vault, and honouring it here dropped this repo's owntools/andskills/from its index. Do not "fix" this by reading it again. ADR 0003 holds the argument and what it rules out — read it before proposing a second ignore source. Which directories are skipped by name, and which losses are counted, islib/walk.mjs's (AMBIGUOUS_SKIP_DIRS,listFiles).- "Stale" has exactly one definition, and it is
indexFreshness, incore/index-file.jssince thegraphMCP tool needed it, and re-exported bylib/open.mjs, which every CLI andtools/cortex-preflight.mjsstill ask.readIndexandINDEX_VERSIONmoved with it. It briefly meant two things, which is worth remembering because neither copy was wrong: preflight's was written when it was the only reachable definition, and the CLIs grew one because they could not reachtools/. Two right answers to one question still disagree the moment one of them changes. Do not add a second. - Import resolution is regex-based, so dynamic and computed imports are missed. That is a
documented limit, not a bug — it is why the orphan finding says "worth checking", never "safe to
delete". A code language
extractImportshas no case for belongs inUNRESOLVED_LANGUAGES(lib/imports.mjs). - One slot per language: a row in
ADAPTERS(lib/resolvers.mjs). Its header holds theprepare/resolvecontract. - Rust: a file no crate root contains is rooted at its own directory (
resolveRustImport). Known limit: a nested helper (tests/index/basic.rsin ripgrep) roots at its own directory rather than at the test crate's, so itscrate::utilfinds nothing. - "Unreferenced" means more than "unimported", and lives in
lib/orphans.mjs. The direction of error is chosen: it can only ever remove entries.findings.mjsandview.mjsboth call it — there is no second copy, for the reasoncoverage.mjssays. - A path alias is read from the repo, never guessed. Never widen this into inferring an alias
from directory names — the value of an edge is that it means something, and resolving
reactto a local file because abaseUrlsat above one is worse than missing the edge. - This gap was invisible to fixtures and obvious on one real repo. A Next.js app wrote 428
imports as
@/…against 104 relative ones: the index held a fifth of its edges and called 154 files orphans, and every consumer — orphans, impact, depth, the viewer — was confidently wrong. Nothing in the test suite could have found it. It happened a second time, the same way: theextendshalf was fixed andreferenceswas never considered, so a stock Vite React-TS app — the layoutnpm create vitegenerates — resolved 13 of its 109 imports and reported 30 unreferenced files. Validate resolver changes against cloned repos, and check that every resolved target actually exists on disk; more edges is not the same as correct edges. - Clusters are not areas, and neither replaces the other (
lib/clusters.mjs, whose header holds the method, the rules and what makes it deterministic). Order is code-unit path order, neverlocaleCompare: a test pins it. Validated on seven cloned repos, each indexed twice to the same bytes. Modularity, clusters against folder areas: zustand 0.445 / 0.045, flask 0.295 / 0.047, fzf 0.196 / −0.262, ripgrep 0.865 / 0.289, a stock Vite React-TS app 0.220 / −0.080, bulletproof-react 0.699 / 0.667. got is the exception, at 0.250 / 0.251. Leiden alone scores 0.341 there, and the large-cluster rule costs the difference: it splits got's 41 tests into groups by the helper they share, which reads better than one cluster calledtest/. Most clusters name one concern: ripgrep's crates, flask's app core, CLI and sessions, zustand'sshallowacrossreact/andvanilla/. fzf's largest (main.goand 18 platform files, cohesion 0.105) does not, because a Go import names a whole package. Clustering took 7.6 ms on bulletproof-react (410 files, 993 edges) and 505 ms on a synthetic 20,000. 37 guards mutated, 36 test failures. The other is the fallback that aggregates by the moved partition when refinement merged nothing. Without it the same answer comes back after up to 64 idle levels. - The graph report (
core/graph.js, re-exported bylib/graph-report.mjs; printed bycortex-graph.mjs) reads clusters and cycles off the index and never recomputes either. Its header holds the rules. It is incore/because thegraphMCP tool returns the same hubs and bridges, and so arelayerKeyFor,isTestDataandinTestSupportDir(core/path-kinds.js),edgeWeightand Tarjan'scomponents;layers.mjs,langs.mjs,clusters.mjsanddepth.mjsre-export them, so no caller here changed. Two departures from the spec were measured, not chosen: the spec's +1 for a low-degree file reaching a hub fired on 27 bridges across nine repos and 26 were the most expected import in the repo, so it is not scored; and a shared dependency or a re-export scores 2 less. Hubs and bridges leave out tests, fixtures, test helpers (inTestSupportDir, shared with the viewer) and vendored files. Files with identical importers and imports fold into one row, which is what makes Go readable. It is information only: one glance line in the findings report, no offer, no finding moved (ADR 0006). Validated on this repo and eight cloned ones: every hub recounted fromindex.edgesand every bridge's edge found on disk, 159 of 159; two runs byte-identical on each. 49 guards mutated, 49 test failures. - Symbols come from an add-on, and the index must not need them (ADR 0029; G4 of the code-graph
spec).
lib/parsers.mjsis the only file that imports a computed path (core/test/install.test.jsholds it to that), and it loads only an install whose every file hashes to the lock (core/parser-lock.js).cortex-index.mjsloads it and hands it tobuildIndexasopts.parsers; every other caller builds without it. Without it the index is byte-for-byte what it was plusparser: "regex": run on seven repos against master, that was the only field that differed. With it,index.symbolssits besidefilesand clusters weigh calls between files too, each pair once at its surest confidence (callEdges).lib/symbols.mjs's header holds what a definition and a call are, the resolution rules and the unresolved reasons.INDEX_VERSIONdid not move: both fields may be ignored, and none changed meaning.- Validated with the add-on installed into a scratch home, on zustand, got, flask and
bulletproof-react:
- Twenty call edges per repo were opened against the source: 80 of 80 right, plus 12 got source-only edges and all 19 of flask's inferred ones.
- Every target is a listed definition, and its line names it.
- Cold cache, warm cache and a third run gave the same bytes on those four and on vite-react-ts, fzf and ripgrep.
- On bulletproof-react, calls moved cluster hubs from barrel
index.tsfiles to the components behind them, and the largest cluster's cohesion went from 0.060 to 0.194. - Mutation-tested: 54 mutants across the integrity check, the destination check, the tar guards and resolution, and 53 caught. The survivor re-verifies the install after writing it, which no input reaches because every byte was hashed before it was written.
- Go, Java and Rust (G5) were validated on fzf, spring-petclinic and ripgrep: sixty sampled
edges read against the source, one wrong (a Go type conversion, now dropped), and cold, warm and
third runs byte-identical. Nine mutants of the new resolution rules, nine caught
(
test/symbols-langs.test.mjs). The package and declared-type rules are insymbols.mjs's header. - The cache key carries a digest of
symbols.mjsitself (EXTRACTOR_VERSION), so an edit there cannot reuse an extraction made by older code. Without it, a fix looked like it had changed nothing, because every file came from the cache.
- Validated with the add-on installed into a scratch home, on zustand, got, flask and
bulletproof-react:
- The route map (
lib/routes.mjs; design and limits indocs/specs/2026-09-27-route-map-design.md) was validated on the Harbor workspace (7/7 calls, 3/3 gateway routes, each to one repo), the RealWorld React + Spring pair (22/22 calls, 19/19 handlers) and spring-petclinic; every citedfile:linewas opened and checked. Since #621 it has two readers,cortex-routes.mjsand the contract below, and reads Express-style, NestJS, FastAPI and Flask handlers and Python calls too. Validated on node-express-realworld (20/20 handlers), nestjs-realworld (21/21, global prefix applied), full-stack-fastapi-template (23/23 handlers, and 23 generated-client calls with the right verbs) and pallets/flask (every example route, none of the five docstring examples in its library source).- Mounts (#630). A router mounted in another file (
app.use("/api", router),include_router(r, prefix=…),register_blueprint(bp, url_prefix=…)) lends its prefix through the import resolverbuildIndexhands toextractRoutes, per file, composed up the chain, and is kept inindex.routes.mounts. A literal prefix isextracted;settings.NAMEwith exactly one string default in the repo isinferredand names itsfromline; anything else is unread with areason, and the paths keep what their file declares. A router mounted in its own file is not followed: the rows do not record which receiver a handler is on. Validated on full-stack-fastapi-template: 0 → 23 of 23 generated-client calls matching a handler,/api/v1inferred fromcore/config.py:22. Unchanged on node-express-realworld, microblog and httpx. Ten mutants, eight caught; one is equivalent (no resolver, no mounts) and the last was caught once a test for a template-literal prefix went in. - Python packages in a subdirectory resolve (
resolveImport,lib/imports.mjs): an absolute import is tried from the nearest ancestor that has no__init__.py, and only that one. Inside a package Python 3 has no implicit relative import, soimport loggingthere is never a siblinglogging.py. The template went from 0 to 22 of 43 Python files with resolved imports; flask gained 4 edges, each checked against the source.
- Mounts (#630). A router mounted in another file (
- The contract (
lib/contract.mjs,cortex-contract.mjs, #621) is what a repo publishes to a team-brain:servesfrom an OpenAPI/Swagger file, else fromindex.routes.handlers;callsfromindex.routes.calls;events;unread. It reads the route map and never detects routes itself — a second detector would disagree with the first. YAML goes throughparseYaml, a subset reader that throwsYamlUnsupported(anchors, merge keys, complex keys) rather than guess; the file is then reported inunread. No line numbers and no query strings, so an unrelated edit is not a contract change. It never writes into a team-brain:mcp/publishes. Validated on swagger-petstore (19/19 operations, ids and response schemas,serversprefix applied), and two extracts of each of five repos were byte-identical. - Story links (
lib/stories.mjs,cortex-stories.mjs, R3 of the role-packs design) are the contract's sibling: which ticket keys this repo's history names, the commits and files each touched, and each file's cluster, hub flag and coverage. What a key is, and whether a message or a branch carries it, iscore/story-key.js, becausemcp/reads the same keys;mcp/publishes.- Confidence is the evidence: a key in a commit message is
extracted, a key only in a branch name isinferred. The branch in a merge subject is the branch's evidence, never the message's. - A PR number is not a story: GitHub's merge and squash subjects, a body line ending
(#N)and a quoted revert suffix areprson the commit. - It refuses a stale index (exit 2), unlike the contract, because every link carries the index's clusters and coverage. It reads git, so outside git it refuses too.
- No commit text and no author is written: a commit is its short sha and its PR numbers.
- Validated on zustand and got, 400 commits each. Ten links were opened against
git show: 8 right, each file list equal to what git says the commit changed. The two wrong are PR numbers named in prose (types dropped in #2462), which no rule can tell from an issue; a mirrored story never holds a PR, soriskdoes not rank them. The first run read zustand's v5 squash body, which lists 20 merged PRs as* title (#N), as 20 stories: the body-line and revert rules came from that, and took zustand from 25 keys to 5. On this repo's 1,203 commits the only Jira-shaped false key wasPSR-4, now a stop prefix.
- Confidence is the evidence: a key in a commit message is
- A file about tests is not a test (
isTestPath,lib/langs.mjs): a runner's setup file in a test directory, and anything under.claude/that is not named as a test. Compared with the old rule over 5,581 files in ten public repos: four files changed, all setup files with no test in them (zustand, three-flatland, create-react-app), and none became a test. Six guards mutated, six failures. - A fixture is test data, not code that needs a test (
isTestData,lib/langs.mjs): code under afixture,fixtures,__fixtures__ortestdatadirectory leaves coverage's denominator. Outside a test directory it is not a test either, so it lends no coverage.inTestPathbeside it, which ranks a secret as a likely fixture, is built from the same list.solutions/stays out: a Next.js app routes it. Run on seven repos: 959 files moved in typescript-eslint, each an AST fixture, its generator or a fixture project's config, none imported from outside a fixture directory; 13 in this repo; none in cli/cli, cobra, got, react-router or jest, whose fixtures sit in test directories already. Eleven guards mutated, eleven failures. - Coverage uses three signals — name, import, and a quoted string mention — and lives in
lib/coverage.mjs, shared byfindings.mjsandimpact.mjs. Each alone misreports: naming alone calledmcp/libuntested when its tests live inmcp/test; a CLI spawned as a subprocess is invisible to both name and import, which is what the mention signal is for. Do not copy this heuristic into a third caller — two copies would agree today and disagree in a month, with nothing to say which is right. - A skill Cortex wrote is checked against the repo it describes, by
lib/skill-drift.mjs. Validated on the first upgraded repo (#462): 12 findings, all real. Theno-script, description andpremisechecks were run on 13 public repos holding 116 skills: one description finding, real (emanote), after a first pass called "canvas/WebGL/Three.js" a path (three-flatland). Mutation-checked: 15 guards, each breaking a test. It also reports amirrorfinding when the.agents/skills/copy of a skill differs from its.claude/skills/source (ADR 0031). cortex-impact.mjsreads the graph backwards — who imports me, not what do I import — and every count it returns is a floor, namedatLeastso a caller cannot print it as a total.--symbol path#nameasks the same of one function, fromindex.symbolsthroughcore/symbol-calls.js. Without symbols it exits 2 and names the parser add-on, never "no callers".--sizerecommends and never decides (lib/sizing.mjs). A finer sizing-local area key was tried and rejected: zustand'svanilla.ts,react.tsandtraditional.tsshare one directory, so no directory key sees that refactor as spread.cortex-reviewcites the lines where a change may have lowered the bar, and never judges them (lib/lowered-bar.mjs; its header lists the five kinds and what it cannot see). It reads the text of the same change set the file list comes from (changedDiff,lib/changed.mjs), and a diff git could not read is reported as unread, never as nothing found. The exit code does not move. Run over 2,990 commits of ten public repos: an import of an assertion library, a runner hook counted as a test, and@ts-expect-errorin a type test were the false positives, and each is a test now. Change a pattern only with a run over real history; the fixtures agreed with every one of those.cortex-review --ruleslists what each rule in the briefs names, and never says a rule is enforced (lib/rule-ledger.mjs; the design and its numbers are indocs/specs/2026-10-12-rule-ledger-design.md). A rule is a list item under a heading that says invariants, gotchas or rules: the sectionstemplates/target-AGENTS.mdstamps. Reading any bold-led item was built first and dropped. On vercel/ai all 21 such items were definitions, and the template's own invariants are plain items. Run on this repo and six public ones: nine rows outside this repo, each opened and each a rule. A rule may name a type instead of a path (ShipmentServiceTest.someMethod), which counts when one indexed file is named for it and the name starts with a capital. That came from the first scaffolded repo it met:/write-area-briefwrote 16 rules into a Java service, seven ended with a real test named that way, and the ledger counted none. After the change all 20 rows there were opened and each is in the right state. 42 guards mutated, 41 test failures, the other an equivalent program. Add a heading word only with real briefs that use it for rules.- Every path two change sets compare goes through
normalizeChangedPath(lib/changed.mjs).--againstitself islib/overlap.mjs's header. - Where ADRs live is
lib/adr.mjs's one answer (isAdrPath,adrLocation); no reader keeps its owndocs/adr/regex. - The page carries nothing about the machine that built it — no absolute root, no home
directory (
scrubPathsinlib/view.mjs). The viewer's other rules — what draws on the Map, onestatsfield per count,safeJson— are in the comments oflib/view.mjsandlib/view-html.mjs. - A single file over the line budget is allowed through as its own batch; only accumulation is bounded.
The loop reader — lib/loop.mjs
Its rules are in its comments and loop.test.mjs; the second-round rows are SECOND_ROUND in
lib/next.mjs.
- Validated on
got(npm),fzf(Make, Go) andflask(Python, nested example manifests), and mutation-tested: nine guards broken one at a time, nine test failures. The watcher, lockfile and hooks rules were validated on bulletproof-react, vitest (root and examples), zustand, the Nest starter, a CRA app, ripgrep and fzf, with nineteen more guards mutated.githubRemotewas read off four GitHub clones (kapi-sprints has no workflows and now waits on its brief alone), with seven guards mutated. Do both again when a row or a detector changes — fixtures here share the author's blind spots.
The Claude-setup checker — lib/claude-setup.mjs
Validated on superpowers, anthropics/skills and anthropics/claude-code; each false positive found
there (a negated "not read-only", [Title](URL), "explain the reasoning", unindented prose in a
description) has a test.
- Two bars for a skill, and the finding says which. A rule from the Claude Code skills page is
about what Claude Code does with the file, and its finding is medium. A rule from the platform's
skill authoring page is a limit of the Agent Skills format that the API and a claude.ai upload
read: Claude Code loads a skill that breaks it. Those three findings (
skill-name-not-portable,skill-description-not-portable,skill-reference-no-contents) are low and their text says Claude Code still loads the skill. Do not raise them. The description limit shows why: the format stops at 1,024 characters and Claude Code truncates at 1,536. "At the top" of a reference file is the first 40 lines, which is Cortex's number and stated in the finding. Run on 292 skills in six public repos: 3 names, 1 description and 22 reference files reported, each opened and each true. - A mod is judged by its files, never by its code. A plugin's
hooks/hooks.jsonwith amoduleskey is a mod. The checker reads the shape ofmodules, whether the path (relative tohooks.json) names a file, and its extension. Which events the module handles and which calls it makes is whatclaude plugin validateprints; a regex copy would disagree with it. Ahooks.jsonthat does not parse is alreadysettings-not-json, so mods have no finding of their own for it. Validated on the four mods in anthropics/claude-code and the three in anthropics/claude-code-playground (each reportsmod-presentand nothing else), with nine guards mutated and nine test failures. - A permission rule is read for its shape, never for what it would match. Three findings read a
project or local settings file: a path rule for
Write,NotebookEdit,MultiEditorGlob(permission-rule-never-matched), adefaultModeofautoorbypassPermissions(default-mode-ignored-in-project), and a.claudeignoreanywhere in the tree (claudeignore-no-effect). A bare tool name is matched at the tool level and is not reported. A plugin'shooks.jsonis not a settings file. Run on eleven public repos and compared with a plain grep of the same files: four of four, three of three and two of two, and nothing in anthropics/claude-code or superpowers. Twelve guards mutated, twelve test failures. - Which frontmatter is unreadable was learned from
claude plugin validate, not from YAML. The validator (2.1.296) repairs some blocks a YAML parser rejects, so each shape was put to it. It rejects a value that ends in a colon, a continuation line holding a colon, and an unindented line that is neither a key nor a list item.readFrontmatterreturnsbrokenfor those. A one-line value holding": "parses with LF endings and is rejected with CRLF, so it comes backokwithfragileset: the two unreadable findings report it and every other check still reads the block. It is reported whatever the endings on this checkout, because the index must not differ between a Windows clone and a Linux one. A duplicate key and an extra key are accepted and stayloose. Across the fourteen plugins in anthropics/claude-code, superpowers and tdd-guard the validator rejects four files andbrokennames the same four; 658 other files with frontmatter in eleven repos report none. Fourteen guards mutated, fourteen test failures. Re-run the probe before changing any of it: the repair is not documented and may change.
How the scoped briefs load — lib/instructions.mjs
- A fact, not a finding. Under a root
CLAUDE.md, Claude Code does not read a subdirectory'sAGENTS.mdon its own. The setting that changes it is ignored in project settings, so no edit to the repo fixes it, andtools/test/cortex-follows-its-own-rules.test.shfails on anything a repo could fix. It is therefore one sentence in the findings report's glance and oncortex-next's brief step, both read fromleavesNote. Do not move it intoclaude-setup.mjs. - Validated on anthropics/claude-code and claude-code-playground (a
CLAUDE.mdand no scoped brief: silent), openai/codex (a scoped brief and noCLAUDE.md: silent) and this repo (four briefs under the shim: stated). Seven guards mutated, seven test failures.
The agents a repo already has — lib/agents.mjs
- Validated by hand on 83 agents in seven public repos (octez-manager, metaxy, spica, kapi-sprints,
a-safe-pulse, Heimdall, posthog). Two mappings are still wrong, and both are recoverable because
the developer confirms every mapping. The answer is kept in
.cortex/agents.json(lib/agent-answers.mjs, whose header holds the rules): reading it may only lose an answer, andcortex-loop.mjs --rememberis its one writer. Checked on kapi-sprints: two unmapped agents answerednone, and the next run asked about neither. octez-manager'sarchitectis a pre-merge architecture reviewer, and the name outranks a description with no job words. spica'spr-review-analystreviews other reviewers' comments. Mutation-checked: 32 guards, each breaking a test.
The team offer — lib/team.mjs
- The debate's round limit is the team's one setting, and its default lives in
stamps.mjs.VALUE_DEFAULTSis what lets a template gain{{DEBATE_ROUNDS}}and still update a file stamped before it; any other new placeholder is refused.CORTEX_REPO(cortex-review.yml) is the other entry: upstream's clone URL, which every file stamped before #622 holds. A first stamp fillsCORTEX_REPOitself fromcore/source.js(withSourceinstamps.mjs) and refuses a directory source; /setup-repo's table does not mention it, because the instruction there measurably lowered that skill's evals.CORTEX_SHA256is filled the same way (withPin, ADR 0028): the digest of this plugin'score/andindex/(cortex-digest.mjs), withCORTEX_REFset tov<VERSION>beside it — a ref for another release is refused. An update of a file stamped before it fills the pair too, which is the one case an update moves a recorded value. The workflow's inline check is a second copy ofcortexDigest;test/cortex-digest.test.mjsruns one against the other.team.mjsreads the limit in this order: theroundsoption, the value in the team skill's record, the default. Onlytemplates/team/team-skill.mdmay hold the placeholder: the roles and the playbook say "the round limit", andteam-templates.test.mjsfails a role that states a number. - Validated on kapi-sprints (offers the architect only; four roles covered), octez-manager (tester
and Project manager;
docs/plans/exists) and zustand (no agents: four roles, the Project manager waits). Mutation-checked withagents.mjs,loop.mjs,stamps.mjsand the CLI: 41 guards, each breaking a test.
Tests
node --test index/test/*.test.mjslib/ is well covered, and since lib/open.mjs exists that now includes argument handling:
open.test.mjs drives the unknown flag, the -v-shaped root, the corrupt index and the version
mismatch from literals, with a fake io so a refusal is an observation rather than a dead test
runner. It used to be reachable only by spawning a real process from a shell fragment, which is why
seven copies of it went unexercised.
Every CLI but cortex-memory.mjs earns a tools/test/cortex-*.test.sh against a real git
fixture, by the rule: does it print a sentence a user will act on, or write into their repo? A
CLI whose only failure mode is a crash does not need one — a stack trace is its own report. The
reason these must be git fixtures and not mkdtemp directories is that git is what decides the
answer: walk.mjs asks it (ADR 0003), churn drives severity, and a non-git fixture can only ever
execute one side of both. Each test's header says what it defends.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

