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
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
- index/AGENTS.mdAGENTS.md
- mcp/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
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
- Very specific: line 3
Read docs/changing-cortex.md first.
- Very specific: line 14
.sh: when a *user* runs it on a machine that may not have Node. - Very specific: line 17
.mjs: when the script reads structured state the product already models — the version sites, the capability floors, the profile policy. - Distinctive: line 138
No test reaches the network.
≈ “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
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
.shwhen a user runs it on a machine that may not have Node.cortex-init.shis 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 frommaster(SECURITY.md)..mjswhen 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.mjsis the clearest case: reimplementingcore/profile.jsin 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:
- names no variable only Claude Code sets:
CLAUDE_PLUGIN_ROOT,CLAUDE_PROJECT_DIR; - runs no Cortex script: no
node,bashorshcommand naming a path undertools/,index/ormcp/, 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; - 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; - 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;
- 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;
- 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.
| Rule | Copies | Pinned by |
| the slug | mcp/lib/slug.js (canonical), _cortex-lib.sh, cortex-init.sh, the browser-side slug() in cortex.sh | mcp/test/slug-parity.test.js |
| the clock | _cortex-lib.sh, cortex-init.sh, cortex-cron.sh | tools/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.sh | tools/test/cortex-root.test.sh — the two shell copies by text, shell against JS by running both over every pairing |
| the Core plugin tier | CORE_PLUGINS in cortex-init.sh, plugins/cortex-core-plugins.json | mcp/test/manifest-parity.test.js |
| the hook-input reader | templates/loop/protected-paths.sh, templates/loop/format-changed.sh, templates/team/test-paths.sh | tools/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
checkUrlhas run the address throughassertDestinationwith kindnpm, 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/parsersas the root.destructive-guard.test.shscans shell tools only, so this file's guard is pinned byindex/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.
| Run | Fails when |
cortex-version.mjs --check | the 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.mjs | a ritual declares no capability floor |
cortex-frontmatter.mjs --check | a 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 --check | a 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 --check | a ritual has no edges at all, in either direction, and declares no metadata: reached-by: |
test/cortex-follows-its-own-rules.test.sh | the 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 --check | the running plugin is behind the repo, so a correct fix looks broken |
cortex-pack.mjs --check | a 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 --check | the 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.
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.

