agentleFS
marinvchmarinvch/Cortex/tools/AGENTS.md

Cortex / tools

AGENTS.md1 starsChanged 18 days agoMITView on GitHub

Read docs/changing-cortex.md first; it holds the invariants that apply to every package. This file owns what is specific to tools/. These are the scripts a person runs, not the ones a ritual imports. core/, index/ and mcp/ are the product; this is the maintenance shelf around it — installers, the viewer generator, the version stamper, the checks that keep the repo honest about itself. Nothing here is imported by the product, which is why core/test/architecture.test.js does not walk this directory.…

  • Deletes or force-pushes

The file

We split this AGENTS.md into the rules it gives an agent

4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read docs/changing-cortex.md first.

    line 3
  2. Very specific:

    .sh: when a *user* runs it on a machine that may not have Node.

    line 14
  3. Very specific:

    .mjs: when the script reads structured state the product already models — the version sites, the capability floors, the profile policy.

    line 17
  4. Distinctive:

    No test reaches the network.

    line 138

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

tools/ — the scripts around the product

Read docs/changing-cortex.md first; it holds the invariants that apply to every package. This file owns what is specific to tools/.

These are the scripts a person runs, not the ones a ritual imports. core/, index/ and mcp/ are the product; this is the maintenance shelf around it — installers, the viewer generator, the version stamper, the checks that keep the repo honest about itself. Nothing here is imported by the product, which is why core/test/architecture.test.js does not walk this directory. That freedom is the reason the rules below have to be written down: the layering test will not catch a mistake here.

.sh or .mjs — decided by the reader, not by taste

  • .sh when a user runs it on a machine that may not have Node. cortex-init.sh is one self-contained file that runs in any repo; that is the whole reason it exists in bash. The docs show it run from a tagged clone, never piped from master (SECURITY.md).
  • .mjs when the script reads structured state the product already models — the version sites, the capability floors, the profile policy. It imports that model instead of re-deriving it. cortex-preflight.mjs is the clearest case: reimplementing core/profile.js in bash would be a fourth copy of the firewall rule.

Importing from core/ is fine and expected. Importing from index/ or mcp/ is a smell — it means a tool is reaching into a leaf, and the shared piece probably belongs in core/.

Two tools run from inside a target repo, not this one. cortex-preflight.mjs and cortex-placeholders.mjs are called by rituals as ${CLAUDE_PLUGIN_ROOT}/tools/... with the target as cwd. Anything they read from Cortex (templates) or print as a command resolves from import.meta.url, never from cwd. Preflight used to print node index/cortex-index.mjs ., a path that does not exist in the repo it was printed to.

cortex-setup.mjs writes into other agents' configs (#623), so its formats are pinned. The logic lives in _setup-agents.mjs (a library: the leading _), whose header holds each agent's config path, format, doc URL and the date it was checked. Re-check those pages before changing a format, and change the fixture in test/fixtures/setup/ in the same commit — test/cortex-setup.test.sh compares every generated file with it. The JSON merge is core/json-text.js, shared with index/lib/shared-plugin.mjs; the TOML half is a scanner that refuses what it cannot read and only ever appends. A committed config never holds a path on one machine: it starts ${CORTEX_HOME}/mcp/server.js, and an agent that expands no variables (Codex) gets user scope instead.

A pack skill is portable

cortex-pack.mjs builds a role pack from skills/ for people who do not run Claude Code, and the design leaves "portable" to be defined. It is defined narrowly here, checked by judgeSkill in _pack.mjs, and each point is failed on a fixture in test/cortex-pack.test.sh. A skill is portable when, after the adaptation, every file it ships:

  1. names no variable only Claude Code sets: CLAUDE_PLUGIN_ROOT, CLAUDE_PROJECT_DIR;
  2. runs no Cortex script: no node, bash or sh command naming a path under tools/, index/ or mcp/, in prose or in a code fence. Outside Claude Code there is no checkout to run them from, and the design sends a pack skill to the team-brain through the MCP tools;
  3. and the skill does not carry disable-model-invocation: true. Other assistants have no such switch, and the flag marks a ritual that must not start by itself;
  4. has frontmatter that parses, a name equal to its folder (at most 64 of a-z, 0-9, -) and a description of at most 1,024 characters;
  5. links only to files that exist: inside its folder as written, outside it to a file in this repository, which the link is rewritten to name on the release tag;
  6. is a text file the check can read (.md, .yaml, .yml, .json, .txt).

The adaptation is mechanical and never touches prose: frontmatter keeps only the Agent Skills keys (name, description, license, compatibility, metadata, allowed-tools), out-of-folder links become release URLs, line endings become LF. A skill that would need its prose rewritten to pass is not packed. It is listed under "needs Claude Code", with the reason, and the fix belongs in the skill. Do not widen the definition to let a skill in: a pack skill that tells an assistant to run node tools/... fails on the first person who tries it, and nothing reports that.

The Claude Desktop entry carries CORTEX_TEAM and CORTEX_TEAM_REPO placeholders beside CORTEX_ROOT. Desktop has no repo, so the team cannot come from a connector; mcp/lib/resolve.js reads the two where none is found. whats-changed stays out of the pm pack because it runs a script; story-map, which is in it, covers the catch-up through the catch_me_up tool.

The copies that are deliberate, and the tests that pin them

Five rules exist in more than one place on purpose, because the files needing them cannot share code. cortex-init.sh is a zero-dependency installer copied into other repos and parses no JSON. tools/server/cortex-cron.sh lands on a server beside only server-setup.sh (it reaches tools/cortex-scrub.mjs by path, or CORTEX_SCRUB, and skips the API call when neither is there — never a bash copy of the gate). The viewer's slug runs in a browser. Each hook template is stamped alone into a repo that has no library to source.

RuleCopiesPinned by
the slugmcp/lib/slug.js (canonical), _cortex-lib.sh, cortex-init.sh, the browser-side slug() in cortex.shmcp/test/slug-parity.test.js
the clock_cortex-lib.sh, cortex-init.sh, cortex-cron.shtools/test/date-parity.test.sh
the root's two names (CORTEX_ROOT, then AI_OS_ROOT)rootFromEnv in core/paths.js (canonical), cortex_root in _cortex-lib.sh, its copy in cortex-cron.shtools/test/cortex-root.test.sh — the two shell copies by text, shell against JS by running both over every pairing
the Core plugin tierCORE_PLUGINS in cortex-init.sh, plugins/cortex-core-plugins.jsonmcp/test/manifest-parity.test.js
the hook-input readertemplates/loop/protected-paths.sh, templates/loop/format-changed.sh, templates/team/test-paths.shtools/test/test-paths.test.sh

Never "improve" one copy alone. The parity test is what makes the duplication safe; without it these drift into two behaviours nobody can tell apart. A new shared helper goes in _cortex-lib.sh and is sourced. A copy with no parity test is not a pattern, it is the failure the parity tests were written about.

A destructive tool routes its target through resolve_in_root

_cortex-lib.sh holds it — the shell counterpart of core/paths.js. Not a string-prefix check: a symlink out of the root passes any prefix comparison and is still an escape. ADR 0010.

destructive-guard.test.sh asserts that over every script here, not over the one the ADR was written about. cortex-sync-skills.sh did rm -rf "${DST:?}/$name" and never sourced the lib; the :? proves the variable is non-empty and resolves nothing, so with .claude/skills pointing out of the repo the loop deleted a directory outside the root and copied canonical into the hole. The test counts a move as destructive too — ADR 0010's original finding was a mv, and a scan watching only rm would have missed the case the rule was written about.

A tool whose deletions cannot take a caller-supplied path opts out in its own header:

# cortex:no-root-guard — <why no path here needs resolving>

The reason is the point, and the test requires one. Four scripts carry it today and each argues a different case (the lib defines the guard; cortex.sh deletes its own mktemp -d; scan-projects deletes $VAULT/projects/$slug.md where slugify() cannot emit a /; vault-extract walks two fixed in-file lists). That last claim used to live in a plan document from 2026-08-18, where no reader of the script would ever find it. A marker on a file that has stopped deleting anything is a failure too — a blanket permission nobody re-reads, the same rule dormant-exemptions.test.sh applies to the secrets marker.

cortex-parsers.mjs is the one tool that downloads code

It installs the parser add-on (ADR 0029), and its header holds the rules. The ones a change must keep:

  • Nothing is requested before checkUrl has run the address through assertDestination with kind npm, and every redirect is checked again before it is followed. Plain http is allowed only to a loopback host, which is what the tests' fake registry is.
  • Nothing is unpacked before the sha512 matches the lock, and nothing is written before every package has been fetched and checked. The install lands in a temporary directory, is verified against the lock, and is renamed into place, so a failure leaves the previous install or none.
  • The tar reader refuses, it never skips. An unsafe name or a link anywhere in the archive refuses the whole package, wanted entry or not, and files are written to paths computed from the lock, never from the archive.
  • A delete goes through resolveInRoot (core/paths.js) with ~/.cortex/parsers as the root. destructive-guard.test.sh scans shell tools only, so this file's guard is pinned by index/test/parsers-install.test.mjs, which also runs the command against a loopback registry. No test reaches the network.

Tests: behaviour, not syntax

bash tools/test/run.sh (add <filter> for one file). Add a case when you touch anything here. bash -n and shellcheck never run a script, which is how four real bugs shipped in tools/server/. Tests build real git repos in temp dirs — a bare repo on disk is a complete remote, so no network.

Four assertions in _helpers.sh, deliberately. A test framework that grows features is a dependency by another name; if a test wants a fifth helper, it is usually the test that wants simplifying.

Every fragment runs under pipefail, set by run.sh where the fragment cannot see it — so nothing pipes into grep -q. grep -q exits at its first match; a writer still flushing takes SIGPIPE, and the pipeline reports 141 for a match that is there. It depends on the scheduler, which is how destructive-guard.test.sh failed once on CI for a file nobody touched (#433). Write grep -q PAT <<<"$(producer)". pipefail-grep.test.sh holds every fragment and every tool that sets pipefail to it.

A *.test.sh file is a fragment run.sh sources, not a script — and it refuses to run alone. The runner exports $WORK and $REPO_ROOT and cds into a fresh temp dir. Standalone, both are empty: cd "$WORK/proj" becomes cd "", which fails without stopping the script, so the git init, the > README.md and the git add -A && git commit after it run against the repository you are in. That is not a hypothetical — on 2026-09-05 bash tools/test/cortex-view.test.sh, typed in exactly the form the briefs above print, put two fixture commits on master, replaced README.md with # readme and rewrote the git identity, all while reporting nothing. So the gate is the first thing in _helpers.sh, every fragment sources it as its first line of code, and every cd carries || exit 1. fragment-guard.test.sh runs every fragment the wrong way on purpose, in a throwaway git repo, and asserts the tree is untouched — plus that a fragment written tomorrow cannot skip the line, which under the runner would otherwise be invisible.

Every test touching the home directory must override the variable the platform actually reads. os.homedir() answers USERPROFILE on Windows and HOME elsewhere, so a fixture that sets only HOME leaves a Windows run reading the developer's real machine — and passing for the wrong reason. plugin-check.test.sh sets both, through one as_home() helper, and hands node a native path because node resolves a bare /tmp/... against the current drive on Windows.

The self-checks, and why each one exists

Each of these guards a failure with no error state — the repo keeps working while quietly lying about itself, so nothing surfaces until a reader is misled.

RunFails when
cortex-version.mjs --checkthe seven version sites disagree. Never hand-edit one; --set writes them all
cortex-release-notes.mjs <x.y.z>that version's changelog section is missing, empty, written twice, or does not end at a lower ## [x.y.z] — date heading. test/release-notes.test.sh runs it on the stamped version, so a section that cannot be published is found in the PR and not while cutting the release
cortex-release-plan.mjs --before <sha>the push must not be released: no usable commit before it, VERSION lowered or not x.y.z, a version site that disagrees, notes that do not extract, or a remote whose tags cannot be listed. It then prints nothing on stdout. release.yml runs it and acts on action=none, exists or release; test/release-plan.test.sh is the only place that logic runs before a real push (ADR 0022)
cortex-capability.mjsa ritual declares no capability floor
cortex-frontmatter.mjs --checka ritual's frontmatter is not flat key: value — the one exception is a metadata: map holding only capability and reached-by — lacks name/description, or holds a value YAML would misread — a block-scalar description parses to | and the skill is never suggested. Strict, with no warn mode. The one frontmatter parser here; cortex-capability.mjs and cortex-skill-graph.mjs import it
cortex-skill-links.mjs --checka markdown link under skills/ names a file that is not there, or a #heading its target does not have. Resolved from the linking file's directory. Links in a code fence or span are examples and are not followed; a run that followed no link at all fails too
cortex-skill-graph.mjs --checka ritual has no edges at all, in either direction, and declares no metadata: reached-by:
test/cortex-follows-its-own-rules.test.shthe claude-setup/ findings Cortex reports on other repos find anything in this one. Fix the repo, not the checker
cortex-skill-usage.mjs --unused(reports, does not fail) which skills the session record shows nobody ever reached
cortex-plugin-check.mjs --checkthe running plugin is behind the repo, so a correct fix looks broken
cortex-pack.mjs --checka role pack holds a skill that is not portable, or a zip it built does not read back. A skill that drops out of a pack is not a failure here; test/cortex-pack.test.sh pins each pack's contents for that
cortex-sync-skills.sh --checkthe gitignored .claude/skills/ mirror has drifted from skills/

The graph check cannot see a front door. It fails only on out.length === 0 && in.length === 0 (cortex-skill-graph.mjs:110), so a ritual keeps passing however thoroughly its inbound edges are stripped — one outbound edge is enough. Keep that &&: a router like /resume-work is nearly all outbound and a shared discipline like /write-agent-docs nearly all inbound, so failing on a low inbound count would force exactly the decorative "see also" edges that make a graph look healthier than it is. The consequence is worth stating plainly, because the surrounding prose implies more: the check proves a ritual is wired to something, not that anything leads to it. It catches total disconnection, which is real and worth catching, and nothing short of it.

Inbound edges are also not equal, and counting them hides that. /connect-mcp and /migrate-legacy-engine had one each until 2.42.0 — from /setup-plugins and /install-project, both causally correct, and both from rituals the session record showed at 0/0. A door behind a door nobody opens; both of those rituals were folded away for it. cortex-skill-usage.mjs is the instrument for that half; no static graph can answer it.

A tool added here that answers a question about the repo should get a --check mode and a test. The report is the point; the exit code is what lets CI or a ritual act on it.

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.