@jaybeeuu/agent-cortex
Personal PI package with custom agents, skills, and extensions
Package details
Install @jaybeeuu/agent-cortex from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@jaybeeuu/agent-cortex- Package
@jaybeeuu/agent-cortex- Version
2.4.1- Published
- Nov 1, 2026
- Downloads
- 1,217/mo · 683/wk
- Author
- jaybeeuu
- License
- MIT
- Types
- extension, skill
- Size
- 1 MB
- Dependencies
- 2 dependencies · 0 peers
Pi manifest JSON
{
"skills": [
"./skills"
],
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
agent-cortex
A personal collection of custom agents and skills, shipped to two harnesses from one source: pi and Claude Code (as a plugin).
Structure
agent-cortex/
├── agents/ # Canonical agents — composable <name>/ dirs (see agents/README.md)
│ ├── ralph/ # composable form (shared agent.md + per-harness pi/, claude/)
│ ├── plan/ # "
│ ├── ralph-plan/ # "
│ └── strategy/ # "
├── skills/ # Skills (grouped by domain) — shared by all harnesses
│ ├── engineering/ # tdd, improve-codebase-architecture, …
│ ├── planning/ # write-a-prd, prd-to-tasks, design-an-interface, …
│ ├── productivity/ # bd-tool, write-a-skill, grill-me, …
│ ├── review/ # review-security, refactor-skill, maintain-agent-docs
│ ├── style/ # style-code, style-tests, style-comms, style-documentation
│ └── workflow/ # ralph, run-pipeline-stage, create-task, …
├── extensions/ # pi extensions (pi only)
│ ├── agent-guardrails/ # behavioural circuit-breakers in the system prompt
│ ├── agent-modes/ # switchable agent modes (reads composable agents/)
│ ├── output-style/ # reply shape: STE, action-first, restated context
│ ├── skill-stats/
│ └── notify/
├── pi/ # Committed pi config templates (see below)
│ ├── settings.json
│ └── keybindings.json
├── token-map.json # canonical tool/path/agent names per harness (install-time token substitution)
├── token-map.README.md # design decisions behind token-map.json
├── bin/
│ ├── agent-cortex.mjs # CLI entrypoint
│ └── installers/
│ ├── claude.mjs # materialises ~/.agent-cortex/claude + ~/.claude/settings.json, registers with Claude Code (--output <dir> = generate-only)
│ └── pi.mjs # stages ~/.agent-cortex/pi + registers it with pi (--output <dir> = stage-only)
├── claude/ # Committed Claude user-settings template (settings.json) — merged into ~/.claude/settings.json
└── claude-extras/ # Hand-authored Claude plugin extras
├── .mcp.json # MCP servers (context7, github) — copied into installs
└── scripts/ # statusline-command.sh — copied into installs (executable)
The same agents/ and skills/ power two harnesses (pi and Claude Code). The composable
agents/<name>/ directories are the single source of truth, and no flat agent file is
committed: agent-cortex install claude and agent-cortex install pi each render their own
from the canonical directories, so install-time output can never drift from the sources.
The {{TOOL:...}} / {{PATH:...}} tokens written in agent and skill files are resolved
per harness at install time from token-map.json, the single source of truth for
canonical tool/path/agent names (see token-map.README.md and the contract section).
The Claude plugin is materialised at install time by bin/installers/claude.mjs. A
plain agent-cortex install claude copies the plugin into the home install root
(~/.agent-cortex/claude), writes a marketplace manifest at
~/.agent-cortex/.claude-plugin/marketplace.json exposing it, and registers it with
Claude Code by driving the claude plugin CLI — state-checked and idempotent (a fresh
install adds the marketplace + installs the plugin; a re-run updates what state says is
out of date; a repeat install at the same version is a no-op; a missing or pre-v2 CLI
warns and prints the manual registration commands instead of failing). The generated
claude/ plugin subtree is never committed (only the claude/settings.json template and
the hand-authored claude-extras/ are), so CI validates the materialiser itself — a
temp-dir install plus structural checks:
- Skills stay single-source — the installer copies each
skills/<group>/<name>/dir flat intoskills/<name>/(Claude discovers skills only one level deep) with{{TOOL:...}}/{{PATH:...}}tokens substituted against token-map.json's claude column — no symlinks, so literal tokens never reach Claude. - Agents can't be shared files (the frontmatter formats differ), so the materialised
plugin's
agents/*.mdare composed from the canonicalagents/<name>/directories'claude/harness dirs (frontmatter.json + section files). Claude only loads agents from a plugin's defaultagents/dir, so the plugin ships them at its own root (default~/.agent-cortex/claude/agents/). - Every agent composes,
ralphincluded. There is no Claude-only agent source: the shared body is harness-neutral and each harness supplies only its own wake mechanism (agents/ralph/<harness>/wake.md), so one body serves both harnesses. - Manifests are generated too: the plugin's
.claude-plugin/plugin.json(itsversiontrackspackage.json, so it never goes stale) andhooks.json(copied from the canonicalhooks/claude/hooks.jsonsource), plus any support files underhooks/claude/bundled into the plugin'shooks/so hook commands can reach them via$CLAUDE_PLUGIN_ROOT. Itsrepositoryis always a string:package.json's object form is reduced to itsurl, then itsdirectory, and dropped when neither exists. Claude rejects any other shape and drops the whole plugin — every skill and agent in it — with that one field. Hand-authored extras —.mcp.jsonandscripts/— are copied into every install from theclaude-extras/dir, which is their canonical store. Seedocs/claude-hooks.mdfor the extension→hook mapping and the rejections (auto-name, agent-guardrails, skill-stats, subagent, agent-modes).
Edit the sources (agents/<name>/ composable dirs, skills/**,
hooks/claude/, claude-extras/, package.json), never anything under the materialised
~/.agent-cortex/claude.
CI
The CI pipeline runs lint, test, and claude-plugin-check as three parallel jobs
(lint, test, claude-plugin-check), each gated on needs: setup. Each job
does its own checkout and pnpm install rather than sharing build artifacts from
the setup job — pnpm workspace symlinks don't survive artifact upload/download,
so artifact sharing would break the workspace resolution that the build depends on.
The root pnpm-workspace.yaml carries a strict allowBuilds map: on pnpm 12 (what CI
pins) any dependency with install or build scripts that is not named there aborts
pnpm install with ERR_PNPM_IGNORED_BUILDS. pnpm 10 only warns, so a newly pulled-in
native dependency surfaces as a red Setup job before it surfaces locally. Adding one
therefore means deciding its build policy in that map — false unless it genuinely
needs to compile, with a comment saying why.
The repo commits no generated claude/ plugin output, so the claude-plugin-check job
validates the Claude plugin materialiser instead of diffing a committed mirror:
it runs node bin/agent-cortex.mjs install claude --output <tmp dir> and checks the
result structurally — plugin.json version tracks package.json, every generated and
hand-authored piece is present, no literal {{TOOL:...}}/{{PATH:...}} tokens survive,
and no symlinks leak into the copied tree. There is no committed agent file to drift-check:
the agents/ tree holds the composable sources only, and both installers render from them.
A separate changeset-check job runs on every pull request and fails it unless the
PR adds a .changeset/*.md file. Publishing is the default and no-publish is the
only opt-out, so shipping nothing has to be a deliberate, reviewable act rather
than an accident. Two exemptions: the changesets bot's Version Packages PR (it
consumes changesets and adds none) and PRs labelled no-publish. The no-publish
label must exist in the repository's label settings — a one-off repo-settings step
that no PR can ship. The style-versioning skill documents the format of a
changeset.
On pushes to main, a release job (gated on lint, test, and
claude-plugin-check) runs changesets to open a chore: version packages PR when
changesets are pending, then publishes to npm once it lands. The version step runs
pnpm version-packages — changeset version, which bumps package.json and rewrites
CHANGELOG.md and touches nothing else (both plugins are materialised at install time
with the package version, so there is no committed output to regenerate); the publish
step runs pnpm publish-package (pack + provenance publish), or
pnpm publish-noop when the merged PR carried no-publish. Labels are invisible on
a push event, so the job resolves the PR merged for the pushed commit through the
GitHub API and reads its labels — no extra secret or scope, since the job already
holds GITHUB_TOKEN and pull-requests: write. A direct push with no merged PR
resolves to no labels and therefore publishes, so attempting to publish an
already-published version still fails loudly; there is deliberately no idempotency
guard. Publishing authenticates via npm Trusted Publishing (OIDC) — no npm token or
GitHub secret is needed, only the one-time npm-side setup on npmjs.com (package →
Access → Trusted Publishing for the jaybeeuu/agent-cortex repo). Releases
are sourced entirely from main.
Merge the version PR whenever its plan and diff look right, but approve its CI
first: changesets/action pushes that branch with GITHUB_TOKEN, so GitHub parks
the CI run the push creates behind “Approve workflows to run” — a run with no
jobs, which reports nothing, so the merge box shows BLOCKED with an empty check
list even though no gate has run or failed. Approving it from the merge box (or
gh api -X POST repos/{owner}/{repo}/actions/runs/<run-id>/approve) starts the five
required checks. Do it after the last merge to main, not once at the start of a
release: every subsequent merge force-updates the version branch, and a new head
brings a new parked run. This stays manual by design — letting the bot's push
trigger CI on its own means a personal access token or GitHub App token, a
long-lived credential in repository secrets to save one click per release.
Installation
Pi package staging (agent-cortex install pi)
agent-cortex install pi materialises agent-cortex as a self-contained pi
package in ~/.agent-cortex/pi — the pi analogue of ~/.agent-cortex/claude —
and registers that directory with pi through pi's own pi install. Nothing is
generated into pi's own directories (~/.pi/agent/…): pi owns its settings
file, so the installer only reads it to decide the plan and lets the pi CLI
write it. Once the staged package is registered, the artifacts an older
installer rendered there are swept (see below).
agent-cortex install pi # stage ~/.agent-cortex/pi + register it with pi
Registration is idempotent by state: the staged path is registered once (a
re-run is a true no-op — a local package source is read from disk on every pi
start, so nothing needs refreshing), and a source pointing at a raw agent-cortex
checkout is removed first, so exactly one agent-cortex source stays configured.
The installer decides that plan by reading pi's settings at
~/.pi/agent/settings.json, so a pi agent dir relocated with
PI_CODING_AGENT_DIR is not inspected — a superseded raw-repo source configured
there has to be removed by hand with pi remove <path>.
Under the old model the same installer rendered agents and skills into
~/.pi/agent/{agents,skills}. Those copies are a second source of the same
resource — the skill-collision diagnostics the staged model exists to remove —
so once registration succeeds the installer removes the ones it can attribute to
agent-cortex: agents/<name>.agent.md and skills/<group>/<name> for agents and
skills this package ships or once shipped, and a .agent-cortex-install.json
path record left there listing those copies. It is a one-time migration:
attribution is by name, which cannot tell a rendered copy from a file you wrote
under the same name, so once it has finished it records that in
~/.agent-cortex/legacy-sweep.json — keyed by the pi user directory, never
inside it — and never removes anything there again. A completed migration
leaves ~/.pi/agent with no agent-cortex file in it. Only a migration that
removed every copy it attributed, from directories it could read, is recorded:
a copy that could not be deleted (permissions, EBUSY) or a directory that
could not be listed is warned about and retried on the next install, and a
record that cannot be written is a warning, not a failed install. Anything
whose name this package does not ship is left alone, and
~/.pi/agent/extensions is never touched at all — no
installer has written there, so its contents are yours. --output <dir> and
--dry-run sweep nothing.
The sweep follows the settings file the installer reads (~/.pi/agent): a pi
agent dir relocated with PI_CODING_AGENT_DIR keeps its copies.
The staged package contains:
| Path | Contents |
|---|---|
package.json |
pi manifest — keywords: ["pi-package"], pi.extensions + pi.skills; the version tracks the package |
agents/ |
composed flat agents (ralph, plan, ralph-plan, strategy) with {{TOOL:...}} / {{PATH:...}} substituted — the shipped agent-modes extension resolves them from the package root |
skills/ |
token-substituted skill copies (real files, never symlinks) |
extensions/ |
copy of the repo extensions/ tree — imports nothing from pi's runtime except the host-aliased typebox, so pi loads it from any directory |
token-map.json |
the token map agent-modes reads from the package root |
node_modules/ |
the required third-party pi packages and their transitive closure |
The third-party packages supply the tools the agents need — pi-questions
(ask_questions) and pi-web-access (fetch_content). They are ordinary
runtime dependencies of this package and travel inside the staged tree, so pi
loads them from a local directory without any npm resolution of its own. An
unresolvable dependency fails the install rather than staging a package that
cannot provide those tools. Re-staging clears the children the installer owns, so
a re-run never accumulates stale copies.
| Flag | Meaning |
|---|---|
--dry-run |
Show what would be staged and registered without writing or spawning anything |
--output <dir> |
Stage into <dir> only — no registration (tests/CI) |
--plugin-root <dir> |
Override the plugin root used for {{PATH:...}} tokens (default: token-map.json's pi value — use it for checkout or symlinked installs) |
--require-register |
Fail the install when the pi CLI cannot drive registration; by default a missing CLI warns and prints the commands to run |
If the pi CLI is not on PATH, the staged package is correct but unregistered:
the installer warns and prints the exact commands (pi install ~/.agent-cortex/pi, plus a pi remove for any superseded raw-repo source).
A superseded raw-repo source that pi remove cannot drop fails the install
(non-zero, naming the source still configured): pi would load both sources, so
reporting success would leave the two-source state registration exists to
prevent. Nothing is installed on that path — remove the source by hand and re-run.
A source whose path no longer exists, and a package the staged bundle ships that
you also registered standalone, are reported and left configured: the installer
never removes an entry you added. Each report gives the pi remove command that
resolves it, with the resolved path, because pi remove resolves a relative
argument against the current directory.
Re-run agent-cortex install pi after every git pull to refresh the staged
package; registration is already in place, so that re-run changes nothing in
pi's settings. Verify with pi list and pi config (restart pi to apply).
Refreshing the installed runtime after a release, and trying uncommitted work without
overwriting it, are recorded in docs/dev-loop.md.
Desktop notifications
Desktop notifications are handled by the local extensions/notify/ extension
(replaces the former pi-notify dependency). It sends an OSC desktop
notification on multi-turn tasks, labelled with the tmux session:window.pane
if available, or the project directory name otherwise.
Pi config files
pi/settings.json and pi/keybindings.json are committed reference
templates for your own pi configuration. They are not installer output: pi's
settings.json and keybindings.json are pi's own user config, so
agent-cortex install pi leaves them alone.
Removing a stale Copilot install
Copilot CLI is no longer a deploy target: agent-cortex install copilot and the
plugin.json manifest are gone, and nothing in this package reads or writes ~/.copilot
any more. No installer sweeps an existing install, so a machine that used the old target
keeps a stale plugin, a cached marketplace, and an instruction block until you remove them
by hand:
rm -rf ~/.copilot/installed-plugins/_direct/agent-cortex
rm -rf ~/.cache/copilot/marketplaces/https---github-com-jaybeeuu-agent-cortex
sed -i.bak '/<!-- agent-cortex:start -->/,/<!-- agent-cortex:end -->/d' \
~/.copilot/copilot-instructions.md
The first two paths are the old install and its marketplace cache. The sed deletes only
the agent-cortex block the removed postinstall hook appended to Copilot's own
instructions file, leaving the rest of that file — and a .bak copy — intact.
Claude Code plugin (separate)
A plain agent-cortex install claude materialises the plugin into the home install
root ~/.agent-cortex/claude — 4 agents (strategy, plan, ralph-plan, ralph),
34 skills (copied flat per skill, {{TOOL:...}} / {{PATH:...}} token-substituted — no
symlinks), SessionStart + Notification hooks, and 2 MCP servers — writes a marketplace
manifest at ~/.agent-cortex/.claude-plugin/marketplace.json exposing ./claude, and
registers it with Claude Code, and merges the committed claude/settings.json template into
~/.claude/settings.json: the template supplies defaults, personal values win, and the
installer owns exactly enabledPlugins + extraKnownMarketplaces — every other key
(permissions, hooks, env, statusLine, unknown keys) is preserved. It also writes the
managed section of ~/.claude/CLAUDE.md — the reply-shape block the pi output-style
extension appends, taken from the same source file — and leaves every other line of that
file alone. The only committed
Claude config is that template plus the hand-authored extras in claude-extras/
(.mcp.json, scripts/); the plugin subtree is generated at install time — there is nothing
to drift.
The generate-only --output <dir> form is for previewing and CI validation; the
documented path is the plain install to ~/.agent-cortex/claude:
node bin/agent-cortex.mjs install claude # materialises ~/.agent-cortex/claude + marketplace manifest AND registers it with Claude Code (user scope, idempotent)
node bin/agent-cortex.mjs install claude --dry-run # plan generation + registration, no writes/spawns
node bin/agent-cortex.mjs install claude --require-register # fail (non-zero exit) when the claude CLI can't drive registration
node bin/agent-cortex.mjs install claude --output /tmp/x # generate only (preview/CI) — no marketplace manifest, no registration
Try it for one session
Generate a throwaway plugin and point Claude Code at it — no install or registration needed:
node bin/agent-cortex.mjs install claude --output /tmp/agent-cortex-claude
claude --plugin-dir /tmp/agent-cortex-claude
# verify what loaded:
claude --plugin-dir /tmp/agent-cortex-claude plugin details agent-cortex
Edits to the generated copy of a SKILL.md are picked up live; re-run the generator after
editing the checkout. Agent, hook, and MCP changes need /reload-plugins.
Trying uncommitted work without overwriting the installed runtime is recorded in
docs/dev-loop.md.
Install persistently (recommended)
The plain install does both halves in one step: it materialises the plugin into
~/.agent-cortex/claude, writes ~/.agent-cortex/.claude-plugin/marketplace.json (the
home install root doubles as the marketplace root — the manifest exposes ./claude), and
registers it with Claude Code by driving the claude plugin CLI against that root at
user scope (the claude plugin install default, so the plugin is available in every
session — the recommended persistent flow). Registration is idempotent by state, not by
exit code: claude plugin marketplace list --json picks add-vs-update and
claude plugin list --json picks install-vs-update against the materialised version. A
fresh install adds the marketplace and installs the plugin; a re-run is the update path
(marketplace update, plus plugin update only when a newer version is materialised); a
repeat install at the same version is a true no-op. Those commands exit 0 on a manifest Claude
rejects, so the installer re-reads claude plugin list --json after they run. A plugin that
list reports as failed to load fails the install with Claude's validation message. A list the
installer cannot read, or one that omits the plugin, warns that the load state is unconfirmed
and prints no success line. It requires the claude plugin CLI
(Claude Code v2+): with a missing or pre-v2 CLI the installer warns and prints the manual
commands below, exiting 0 — --require-register makes registration mandatory and fails
non-zero when it can't run:
agent-cortex install claude # materialise + register (user scope, idempotent)
agent-cortex install claude --require-register # register or fail the install
--dry-run prints the full plan without spawning the claude CLI or writing anything;
--output <dir> generates only, with no marketplace manifest and no registration.
The install also merges the committed claude/settings.json template into
~/.claude/settings.json (the settings file Claude Code reads at user scope). The template
supplies defaults and personal values win; the installer owns exactly enabledPlugins +
extraKnownMarketplaces, so permissions, hooks, env, statusLine and unknown keys are
never touched. A legacy symlinked settings.json is replaced with a real file rather than
written through, and --output leaves the user's Claude config alone.
It also writes the managed section of ~/.claude/CLAUDE.md (Claude's user memory), delimited
by <!-- BEGIN agent-cortex:output-style --> / <!-- END agent-cortex:output-style -->. The
content is extensions/output-style/output-style.md — the same file the pi extension appends
to its system prompt — so both harnesses carry one copy of the rules. Text outside the
markers is never touched; a file with only one marker is left unchanged with a warning.
The equivalent manual registration adds a marketplace root by absolute path (a bare . is
rejected) — the home install root (~/.agent-cortex) is the marketplace root written by
the plain install; the repo checkout ships no manifest:
claude plugin marketplace add ~/.agent-cortex # after a plain install
claude plugin install agent-cortex@jaybeeuu # every session (user scope)
# or, for this project only:
claude plugin install agent-cortex@jaybeeuu --scope local
After installing, the agents and skills are available in every session with no --plugin-dir
flag, and Ralph is just claude --agent agent-cortex:ralph.
Update
The plain install re-materialises from the current sources, so after pulling changes
re-run it — it refreshes the materialised plugin and re-registers whatever the installed
state says is out of date (marketplace update, plus plugin update only when a newer
version is materialised; a repeat install at the same version is a no-op) in one step:
git pull
agent-cortex install claude # re-materialise + refresh (restart Claude Code to apply)
Registration matches the marketplace by name, not path: a marketplace previously added
from a different root is refreshed in place rather than re-pointed at the fresh home
install. Re-point it with
claude plugin marketplace remove jaybeeuu and re-run the install.
The plain install IS the update — there is no separate build step. (--output <dir> only
generates a preview and never refreshes an installed plugin.)
Uninstall
claude plugin uninstall agent-cortex
claude plugin marketplace remove jaybeeuu
Using the agents
| Agent | How to invoke | Purpose |
|---|---|---|
strategy |
delegate: "use the strategy agent…" | Vision brief / PRD / technical-direction docs |
ralph-plan |
delegate: "use the ralph-plan agent…" | Explore, grill, and file beads for a change |
plan |
delegate: "use the plan agent…" | End-to-end planning (PRD → classified beads) |
ralph |
run as the main agent: claude --agent agent-cortex:ralph |
Parallel backlog orchestrator (below) |
Skills auto-trigger from their descriptions, or invoke one explicitly as
/agent-cortex:<skill> (e.g. /agent-cortex:tdd).
The lean Ralph runs as the interactive main agent (not delegated — it must stay alive to receive its workers' completions):
claude --agent agent-cortex:ralph # (omitted once installed via agent-cortex install claude; else --plugin-dir ~/.agent-cortex/claude)
It finds ready beads, spawns parallel background workers (implement → independent review →
fix), opens a PR per feature, and pauses at each human merge gate. After you merge, re-invoke
it and it resumes from bd ready.
Hooks
SessionStart injects a per-session policy nudging Claude to prefer the shipped skills over
ad-hoc choices: the "style policy" (invoke style-code / style-tests /
style-documentation / style-comms before the corresponding work — their descriptions also
auto-trigger proactively) and the "skill policy" (using-agent-skills for routing, bd-tool
for beads context, git-workflow for branch/PR discipline). A Notification hook matched on
agent_completed|agent_needs_input|permission_prompt raises a desktop notification when a
task finishes, waits on input, or needs approval. See docs/claude-hooks.md for the full
extension→hook audit.
Not ported / follow-ups
- Only
session-startandnotifyhad Claude equivalents (both ported to hooks); the other piextensions/(auto-name, agent-guardrails, skill-stats, subagent, agent-modes) have none —output-stylereaches Claude through the managedCLAUDE.mdsection instead; the audit and rejection rationale live indocs/claude-hooks.md. - Ralph follow-ups: multi-feature epic branches, and a GitHub-trigger routine to auto-resume after a PR merge (instead of manual re-invocation).
- Ralph's per-task pipeline is shared, not ported:
ralphandrun-pipeline-stageship to Claude and compose from the same harness-neutral body as pi, differing only in the wake section.
Contributing to the Claude plugin
Edit the sources — the composable agents/<name>/ directories (shared agent.md +
per-harness frontmatter/sections, composed by bin/installers/claude.mjs), skills/**,
hooks/claude/hooks.json (hook config), claude/settings.json (the user-settings template
merged into ~/.claude/settings.json), claude-extras/ (hand-authored .mcp.json and
scripts/), and package.json (plugin
version — picked up at install time) — then run
agent-cortex install claude (re-materialises the home plugin, merges settings, and
re-registers it; --output <dir> generates a preview only). The only committed file under
claude/ is the settings template; ~/.agent-cortex/claude is generated in full
(.claude-plugin/plugin.json, agents/, skills/, hooks.json, .mcp.json, scripts/)
so never edit anything under it. CI validates the Claude materialiser (structural checks on
a temp-dir install) so sources and installer can never silently diverge.
Migrating a beads workspace
agent-cortex migrate brings the .beads workspace it runs in — any repo, not just this
one — up to the latest agent-cortex migration version. The current version is derived from
the workspace's own state (the first step with pending work), so there is no marker file to
drift and a re-run after an apply is a no-op. Step v1 renames implementation-type:afk|hitl
labels to execution:afk|hitl on every bead carrying them, closed ones included: the goal
is an empty legacy namespace, not just a clean live one (see
docs/technical-direction/migration-execution-labels.md for the two-axis model behind it).
A bare agent-cortex migrate is a dry run — it prints the per-bead plan and writes
nothing. --apply runs every pending step in order and verifies each before moving on. It
shells out to the bd CLI on PATH (BD_PATH overrides) and fails loudly, rather than
half-applying, when bd is missing or a bd subcommand errors.
Detection reads bd export --all and cross-checks bd label list-all, never a
label-filtered bd list: on bd 1.3.1 that query can omit closed beads, which once let a
partially migrated workspace report itself as verified. When the label index still reports a
legacy label that no exported bead carries — or a legacy label survives the apply — migrate
names the beads and exits non-zero.
agent-cortex migrate # dry run — per-bead plan, zero writes
agent-cortex migrate --apply # apply pending steps, verifying each