@jaybeeuu/agent-cortex

Personal PI package with custom agents, skills, and extensions

Packages

Package details

extensionskill

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 into skills/<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/*.md are composed from the canonical agents/<name>/ directories' claude/ harness dirs (frontmatter.json + section files). Claude only loads agents from a plugin's default agents/ dir, so the plugin ships them at its own root (default ~/.agent-cortex/claude/agents/).
  • Every agent composes, ralph included. 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 (its version tracks package.json, so it never goes stale) and hooks.json (copied from the canonical hooks/claude/hooks.json source), plus any support files under hooks/claude/ bundled into the plugin's hooks/ so hook commands can reach them via $CLAUDE_PLUGIN_ROOT. Its repository is always a string: package.json's object form is reduced to its url, then its directory, 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.json and scripts/ — are copied into every install from the claude-extras/ dir, which is their canonical store. See docs/claude-hooks.md for 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-start and notify had Claude equivalents (both ported to hooks); the other pi extensions/ (auto-name, agent-guardrails, skill-stats, subagent, agent-modes) have none — output-style reaches Claude through the managed CLAUDE.md section instead; the audit and rejection rationale live in docs/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: ralph and run-pipeline-stage ship 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