agentleFS
marinvchmarinvch/Cortex/index/AGENTS.md

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…

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
  1. Very specific:

    The index is deterministic: No LLM, no network, no clock, no randomness.

    line 47
  2. Distinctive:

    Do not add a second.

    line 127
  3. Very specific:

    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 …

    line 140
  4. Distinctive:

    Every target is a listed definition, and its line names it.

    line 195
  5. Very specific:

    Run on this repo and six public ones: nine rows outside this repo, each opened and each a rule.

    line 308
  6. Very specific:

    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 …

    line 443

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

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/. findings returns data; /write-context-docs is 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 is cortex-stamps.mjs, in two commands. stamp writes one file /setup-repo was 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 --force carries the developer's yes. update rewrites a file Cortex itself stamped, and only in state update — untouched since it was recorded, re-renderable, template changed. /setup-repo runs 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 to edited, conflict or review. And never from an older plugin: when the record's cortex is newer than the running VERSION, its templates are the older ones, every untouched file reads as update, and applying it puts the older template back. planUpdates refuses the whole plan then, and takes running as a required argument so the check cannot be forgotten; status, diff and next.mjs name 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.json on 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-update adds "autoUpdate": true only on a cortex entry this run adds. An entry already there keeps its autoUpdate — true, false or unset — because the committed value outranks each teammate's own /plugin toggle, and someone chose it. Without the flag the key is never written. The third, cortex-section.mjs --replace, rewrites one section of CLAUDE.md, and only in state outdated (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 to edited. The fourth, ensureGitignored (lib/generated.mjs), appends the generated directories a target's .gitignore lacks when a CLI writes under that repo's .cortex/ — append only, never .cortex/memory/ (it is committed), and nothing at all when --out points 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 through resolveInRoot. Never let it take a file that is not markdown.
  • Tests that count loop rows delete CORTEX_PROFILE at the top of the file, because a developer on a work profile must get CI's answers. Which repo is a team's is teamServed (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.mjs asserts 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 is LOOP_STAMPS in lib/loop.mjs — do not keep a second list.
  • What the stamp record says is read once, by stampReport. cortex-stamps.mjs . --json prints it and lib/next.mjs ranks from it. A new fact about the record goes in the report, never in one of the two readers. TEMPLATES_DIR is exported from lib/stamps.mjs for the same reason: four files each resolved the plugin's templates/ folder.
  • A command reads the repo once. One that holds a plan prints its last line with loopLineOf or nextLineOf; loopLine and nextLine read the repo, and are for a command that has no plan (cortex-index, and cortex-findings after it wrote its report). findings.mjs takes contextLayer, never readState, which also plans the loop and asks git. indexed is two facts under one name: in the loop state an index was handed in, in readState the file is on disk.
  • A section Cortex appends to a shared file (current · outdated · edited · kept) is defined in the headers of lib/section.mjs, lib/sections.mjs and lib/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 read edited.
  • Every CLI here opens through lib/open.mjs, and declares its flags rather than testing for them — all but cortex-memory.mjs, which takes a subcommand and a --root that 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 ninth parseArgs — 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). readIndex also checks INDEX_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.json has one reader — readEnrichment, defined in core/index-file.js and re-exported by lib/enrich.mjs — and it returns a state, not a document. ok is the only one a caller may trust; the states, their notes and why staleness never points at merge are in its JSDoc. The viewer declines stale enrichment — the policy and its argument are in cortex-view.mjs.
  • findings.mjs and next.mjs ask has(ENRICHED_REL) and that is correct — "was this step ever run" is a question about a file existing, and next.mjs may 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 asks core/memory.js, which reads both layouts — a day file <date>.md, and one file per author in a day directory, <date>/<author>.md. That is cortex-memory.mjs recent, the viewer's timeline (readMemory in lib/overview.mjs, drawn by lib/view-html.mjs), and the memory row of cortex-next and the loop state (memoryFiles in lib/loop.mjs, which lib/next.mjs shares). next.mjs and loop.mjs listed *.md in 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's author is the slug from its path, or null, and the page shows nothing for null.
  • cortex-memory.mjs append passes no author; core/memory.js finds 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 one wrote line, and one line on stderr says so on every such write. A CORTEX_AUTHOR that is set and unusable writes nothing and exits 1. A test that runs the CLI sets CORTEX_AUTHOR or isolates git (isolatedGit in test/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.mjs tells a hallucinated path from a renumbered batch is in validateBatch.

Gotchas

  • Vendored is declared in .gitattributes, never guessed by Cortex. Same rule as go.mod and composer.json: declared beats guessed. briefCandidates and isEnrichable skip it and stats.vendored names 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 in stats.skipped, and indexes it as source where the repo declares -linguist-vendored there (VENDOR_DIR, declaredOwn). Indexing every tracked vendor/ as vendored was measured and rejected (#529). The rest is lib/vendored.mjs's header.
  • walk.mjs asks git, not .cortexignore. Those answer different questions: .cortexignore says what is not knowledge in a vault, and honouring it here dropped this repo's own tools/ and skills/ 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, is lib/walk.mjs's (AMBIGUOUS_SKIP_DIRS, listFiles).
  • "Stale" has exactly one definition, and it is indexFreshness, in core/index-file.js since the graph MCP tool needed it, and re-exported by lib/open.mjs, which every CLI and tools/cortex-preflight.mjs still ask. readIndex and INDEX_VERSION moved 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 reach tools/. 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 extractImports has no case for belongs in UNRESOLVED_LANGUAGES (lib/imports.mjs).
  • One slot per language: a row in ADAPTERS (lib/resolvers.mjs). Its header holds the prepare / resolve contract.
  • Rust: a file no crate root contains is rooted at its own directory (resolveRustImport). Known limit: a nested helper (tests/index/basic.rs in ripgrep) roots at its own directory rather than at the test crate's, so its crate::util finds 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.mjs and view.mjs both call it — there is no second copy, for the reason coverage.mjs says.
  • 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 react to a local file because a baseUrl sat 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: the extends half was fixed and references was never considered, so a stock Vite React-TS app — the layout npm create vite generates — 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, never localeCompare: 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 called test/. Most clusters name one concern: ripgrep's crates, flask's app core, CLI and sessions, zustand's shallow across react/ and vanilla/. fzf's largest (main.go and 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 by lib/graph-report.mjs; printed by cortex-graph.mjs) reads clusters and cycles off the index and never recomputes either. Its header holds the rules. It is in core/ because the graph MCP tool returns the same hubs and bridges, and so are layerKeyFor, isTestData and inTestSupportDir (core/path-kinds.js), edgeWeight and Tarjan's components; layers.mjs, langs.mjs, clusters.mjs and depth.mjs re-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 from index.edges and 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.mjs is the only file that imports a computed path (core/test/install.test.js holds it to that), and it loads only an install whose every file hashes to the lock (core/parser-lock.js). cortex-index.mjs loads it and hands it to buildIndex as opts.parsers; every other caller builds without it. Without it the index is byte-for-byte what it was plus parser: "regex": run on seven repos against master, that was the only field that differed. With it, index.symbols sits beside files and 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_VERSION did 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.ts files 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 in symbols.mjs's header.
    • The cache key carries a digest of symbols.mjs itself (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.
  • The route map (lib/routes.mjs; design and limits in docs/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 cited file:line was opened and checked. Since #621 it has two readers, cortex-routes.mjs and 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 resolver buildIndex hands to extractRoutes, per file, composed up the chain, and is kept in index.routes.mounts. A literal prefix is extracted; settings.NAME with exactly one string default in the repo is inferred and names its from line; anything else is unread with a reason, 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/v1 inferred from core/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, so import logging there is never a sibling logging.py. The template went from 0 to 22 of 43 Python files with resolved imports; flask gained 4 edges, each checked against the source.
  • The contract (lib/contract.mjs, cortex-contract.mjs, #621) is what a repo publishes to a team-brain: serves from an OpenAPI/Swagger file, else from index.routes.handlers; calls from index.routes.calls; events; unread. It reads the route map and never detects routes itself — a second detector would disagree with the first. YAML goes through parseYaml, a subset reader that throws YamlUnsupported (anchors, merge keys, complex keys) rather than guess; the file is then reported in unread. 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, servers prefix 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, is core/story-key.js, because mcp/ 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 is inferred. 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 are prs on 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, so risk does 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 was PSR-4, now a stop prefix.
  • 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 a fixture, fixtures, __fixtures__ or testdata directory leaves coverage's denominator. Outside a test directory it is not a test either, so it lends no coverage. inTestPath beside 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 by findings.mjs and impact.mjs. Each alone misreports: naming alone called mcp/lib untested when its tests live in mcp/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. The no-script, description and premise checks 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 a mirror finding when the .agents/skills/ copy of a skill differs from its .claude/skills/ source (ADR 0031).
  • cortex-impact.mjs reads the graph backwards — who imports me, not what do I import — and every count it returns is a floor, named atLeast so a caller cannot print it as a total. --symbol path#name asks the same of one function, from index.symbols through core/symbol-calls.js. Without symbols it exits 2 and names the parser add-on, never "no callers".
  • --size recommends and never decides (lib/sizing.mjs). A finer sizing-local area key was tried and rejected: zustand's vanilla.ts, react.ts and traditional.ts share one directory, so no directory key sees that refactor as spread.
  • cortex-review cites 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-error in 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 --rules lists what each rule in the briefs names, and never says a rule is enforced (lib/rule-ledger.mjs; the design and its numbers are in docs/specs/2026-10-12-rule-ledger-design.md). A rule is a list item under a heading that says invariants, gotchas or rules: the sections templates/target-AGENTS.md stamps. 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-brief wrote 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). --against itself is lib/overlap.mjs's header.
  • Where ADRs live is lib/adr.mjs's one answer (isAdrPath, adrLocation); no reader keeps its own docs/adr/ regex.
  • The page carries nothing about the machine that built it — no absolute root, no home directory (scrubPaths in lib/view.mjs). The viewer's other rules — what draws on the Map, one stats field per count, safeJson — are in the comments of lib/view.mjs and lib/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) and flask (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. githubRemote was 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.json with a modules key is a mod. The checker reads the shape of modules, whether the path (relative to hooks.json) names a file, and its extension. Which events the module handles and which calls it makes is what claude plugin validate prints; a regex copy would disagree with it. A hooks.json that does not parse is already settings-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 reports mod-present and 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, MultiEdit or Glob (permission-rule-never-matched), a defaultMode of auto or bypassPermissions (default-mode-ignored-in-project), and a .claudeignore anywhere in the tree (claudeignore-no-effect). A bare tool name is matched at the tool level and is not reported. A plugin's hooks.json is 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. readFrontmatter returns broken for those. A one-line value holding ": " parses with LF endings and is rejected with CRLF, so it comes back ok with fragile set: 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 stay loose. Across the fourteen plugins in anthropics/claude-code, superpowers and tdd-guard the validator rejects four files and broken names 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's AGENTS.md on its own. The setting that changes it is ignored in project settings, so no edit to the repo fixes it, and tools/test/cortex-follows-its-own-rules.test.sh fails on anything a repo could fix. It is therefore one sentence in the findings report's glance and on cortex-next's brief step, both read from leavesNote. Do not move it into claude-setup.mjs.
  • Validated on anthropics/claude-code and claude-code-playground (a CLAUDE.md and no scoped brief: silent), openai/codex (a scoped brief and no CLAUDE.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, and cortex-loop.mjs --remember is its one writer. Checked on kapi-sprints: two unmapped agents answered none, and the next run asked about neither. octez-manager's architect is a pre-merge architecture reviewer, and the name outranks a description with no job words. spica's pr-review-analyst reviews 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_DEFAULTS is 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 fills CORTEX_REPO itself from core/source.js (withSource in stamps.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_SHA256 is filled the same way (withPin, ADR 0028): the digest of this plugin's core/ and index/ (cortex-digest.mjs), with CORTEX_REF set to v<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 of cortexDigest; test/cortex-digest.test.mjs runs one against the other. team.mjs reads the limit in this order: the rounds option, the value in the team skill's record, the default. Only templates/team/team-skill.md may hold the placeholder: the roles and the playbook say "the round limit", and team-templates.test.mjs fails 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 with agents.mjs, loop.mjs, stamps.mjs and the CLI: 41 guards, each breaking a test.

Tests

node --test index/test/*.test.mjs

lib/ 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.