Updates (prompted self-update)
Chief of Staff ships as a folder the principal downloads and opens in their host. An update only replaces the shipped files (brain/ + AGENTS.md, CHIEFOFSTAFF.md, CLAUDE.md, VERSION, CHANGELOG.md, README.md, .claude/commands/) — it never touches memory/ (the principal's data) or .claude/settings.local.json. So updating is safe and their context carries over untouched.
Everything below is host-agnostic: it needs only the ability to fetch a file and write into this folder — which every supported host with an execution surface has (Claude Code's shell, or code execution in Codex, in the ChatGPT desktop app). If the host can't fetch or has no network, skip silently and fall back to the manual path (README → download the latest ZIP and replace brain/).
Where things live
All three are published to the public GitHub Pages mirror (docs/dist/, refreshed on every release), so the check and the apply work even while the source repo stays private — GitHub Release assets on a private repo aren't publicly downloadable, but the Pages site is:
- Latest version pointer:
https://chiefofstaff.team/dist/VERSION(the version of the latest published release). - Latest release archive (stable URL):
https://chiefofstaff.team/dist/cos.zip— flat: it unzips its contents straight into the folder, with no wrapper directory. (https://chiefofstaff.team/dist/ChiefOfStaff.zipis the same release wrapped in aChiefOfStaff/directory. It exists only so installs predating 0.20.0 — whose copy of this recipe hardcodes that path — keep updating. Don't use it here; it will be retired.) - What's-new: the
## [x.y.z]section ofCHANGELOG.md—https://chiefofstaff.team/dist/CHANGELOG.md(or the local copy).
The check (gated — don't nag)
Run at boot (see CHIEFOFSTAFF.md boot protocol), at most once per day and only if the principal hasn't turned it off (update-check: off in memory/preferences/updates.md; default on):
- If the local
VERSIONends in-dev, skip the check. A-devbuild is a development checkout ofmain, already ahead of the latest release; refresh it however it was obtained, not with/update. Released builds always carry a clean version, so end users are unaffected. - Read the local
VERSION. - Fetch the latest release's
VERSION(the release-asset URL above). No network / no execution surface (or the release can't be reached) → skip silently. - Record the check date (
last_update_check: <YYYY-MM-DD>inmemory/preferences/updates.md) so you don't re-check the same day. - If the latest is newer than local → prompt once: name the new version and the CHANGELOG highlights, and ask if they'd like to update now (their memory is untouched). Don't auto-update. If they decline, don't re-ask until a still-newer version appears.
Applying an update (/update, on the principal's OK)
Operate in this Chief of Staff folder. What follows is a contract, not a script — carry it out with whatever this host actually has (a POSIX shell, PowerShell, Python, or the host's own file-write API), the same way a prompt boot is a sentence rather than a command. Each step is a property that must hold when you're done; the commands below are one way to get there, not the definition.
- Fetch
https://chiefofstaff.team/dist/cos.zipinto a scratch directory outside this folder. Send an ordinaryUser-Agent— the CDN answers 403 to default library agents (a barePython-urllib/3.xis refused;curlis fine), and a 403 body saved ascos.zipfails at step 2. - Unpack it, then find the payload — the directory that directly contains
AGENTS.md. The archive unpacks flat today; check for a single wrapper directory too. If you can't findAGENTS.md, stop and change nothing — an unrecognized layout must never half-update a folder. - Replace
brain/wholesale: remove the existing directory, then copy the new one. Overwriting in place is wrong — it leaves files the new release deleted, and a stale playbook the index no longer lists is worse than a missing one. - Replace
.claude/commands/the same way: remove, then copy. - Overwrite the boot files:
AGENTS.md,CHIEFOFSTAFF.md,CLAUDE.md,VERSION,CHANGELOG.md,README.md. - Copy nothing else. An update never touches
memory/or.claude/settings.local.json— the principal's files stay exactly as they are. - Delete the scratch directory.
Reference implementation, POSIX shell:
tmp="$(mktemp -d)"
curl -fsSL -o "$tmp/cos.zip" https://chiefofstaff.team/dist/cos.zip
unzip -q "$tmp/cos.zip" -d "$tmp"
# Locate the payload rather than assuming its shape: cos.zip unpacks flat, but detect a wrapper
# directory too, so a future change to the archive can never strand an install on this recipe.
new="$tmp"
[ -f "$new/AGENTS.md" ] || { for d in "$tmp"/*/; do [ -f "$d/AGENTS.md" ] && new="${d%/}" && break; done; }
[ -f "$new/AGENTS.md" ] || { echo "update: unrecognized archive layout, aborting" >&2; exit 1; }
rm -rf brain && cp -R "$new/brain" ./ # brain (delete-then-copy: drops removed files)
cp "$new/AGENTS.md" "$new/CHIEFOFSTAFF.md" "$new/CLAUDE.md" "$new/VERSION" "$new/CHANGELOG.md" "$new/README.md" ./
rm -rf .claude/commands && cp -R "$new/.claude/commands" .claude/
rm -rf "$tmp"
On Windows without a bash shell, PowerShell does the same job — Invoke-WebRequest (step 1), Expand-Archive (2), Remove-Item -Recurse then Copy-Item -Recurse (3 and 4). Same contract.
- On a host with no execution surface at all, use its file-write capability, or hand the principal the one-line manual path (README).
After updating
- Read the new
VERSIONand confirm it bumped. - Report the what's-new highlights from
CHANGELOG.md. Don't apply any memory migration here — this session is stale (still on the pre-upgrade brain), so migrations run automatically at the first new session, driven by the new brain (see "Applying memory migrations" below). If the newCHANGELOG.mdshows migration notes for the span, tell the principal a one-liner — that their next fresh session will apply them (their memory is untouched until then). - Enter stale-session mode. The update changed the files on disk, but this session is still running the brain it loaded at boot — and re-reading the new files wouldn't fix that: the old brain is still in context, so a re-read layers a conflicting copy on top of it and refreshes only the always-load tier, leaving the on-demand playbooks you already pulled at the old version. (This is why
CHIEFOFSTAFF.md→ RELOAD, which re-grounds after a compaction, explicitly does not clear stale mode.) The update takes effect on a fresh session: a new session re-runs the boot protocol, loads the new brain, and applies any pending migration. So from now on, prefix every reply with the stale-session banner — that banner is the ongoing prompt to start a fresh session, not a second nag on top of it. The rule and the exact banner text live inCHIEFOFSTAFF.md→ SESSION FRESHNESS (single source; don't restate the banner here). Reassure them this session is safe to keep using — it just won't have the new behavior (or the migration) until it's reloaded. "Start a fresh session" is the concept on both hosts; don't hardcode one host's UI.
Applying memory migrations (after an upgrade)
A memory migration runs at the first new session after an upgrade — never in the session that ran /update (that one is stale: still on the pre-upgrade brain, so it can't reliably migrate against the new schema, and a stale session is frozen from writing memory anyway; see CHIEFOFSTAFF.md → SESSION FRESHNESS). The new session's boot (step 2) is the single choke point for every upgrade path — /update or a manual ZIP replace.
Trigger: at boot, memory/meta.md onboarded_against is older than VERSION. Compare numerically, component by component — never as text. 0.9.0 is older than 0.10.0 and older than 0.22.0, though a text comparison claims the opposite; getting it backwards skips this pass in silence, on exactly the installs with the most to migrate. This session is current (baseline == VERSION), so it may write memory. Run the pass as the first order of business, in version order:
- Establish the span, and say it. From =
memory/meta.md→onboarded_against; to =VERSION. Ifonboarded_againstis missing, the span starts at the earliest release inCHANGELOG.md. That window — "0.20.0 → 0.22.0" — is what you assess, and it goes in the proposal (step 4) so the principal can see what was considered rather than taking it on trust. - Read every release entry in the span, not just its migration notes. Notes say what to do and take precedence where they exist — but they are a curated hint, not a complete record. Releases have shipped changes to what memory holds with no note at all (0.15.0 removed the per-member
clearance:/## Readscharter fields and said nothing). So read the entries too, and treat any that describes a renamed or removed field, a moved or retired file, or a changed convention as a candidate migration even where no note flags it. Match whichever form a release used: a### Migration notesblock, a legacy### Migrationblock, or an inline**Migration:**/ "no migration required" statement. - Resolve the overlay. Read
memory/overrides/index.md; any brain playbook this pass invokes (a versioned migration,/lint) is resolved againstmemory/overrides/like any other brain file, so a principal's override still applies during migration. Reconciling orphaned overrides (step 6) is what lets you rely on the overlay safely — it runs before a possibly-dead override could mislead you. - Assess each candidate against the actual folder, then propose once. A change to something this install doesn't have is not a migration for this principal — check before you raise it: is there a roster? do charters carry a
## Method, or a staleclearance:/tools:? are there leftoverAGENTS.md/CLAUDE.mdin a member folder, amemory/.gitkeep, an orphaned override?
If anything that applies would write the principal's content — greet in one line, then put the whole thing in a single exchange: the span, what you would change and why, and what you checked and found nothing for. Write nothing until they say yes. Say plainly that declining a required step just means you'll raise it again next session, so a "no" is visibly not permanent.
If nothing that applies would write — say nothing. Advance the marker and ledger it (step 8) and get on with the session; don't turn a no-op into noise.
Your own bookkeeping is not a content write: advancing onboarded_against and appending the update ledger line happen either way and need no permission. What needs proposing is anything that changes what the principal wrote or owns — charters, notes, the index, overrides.
- Run any versioned migration a note points to (required) — e.g. the pre-roster team migration (
brain/playbooks/team.md→ "Migrate a pre-roster team"). Idempotent: safe if already done. It may append its own ledger line in addition to the span line in step 8. - Reconcile orphaned overrides (required). For each
## Replace/## Extendentry inmemory/overrides/index.md, verify its mirroredbrain/path still exists in the new brain. For any that don't (the brain file the override targeted was removed), flag it — name the override, the vanished target, and the release that removed it (perCHANGELOG.md) — and offer the principal a choice: re-home it to a still-live target, convert it to a pure addition (move the entry to## Additions, dropmode:/base_version:from its frontmatter), or retire it. Never migrate an orphan silently — the delete-then-copy update mechanics above would otherwise leave it looking like an intentional addition (seebrain/schemas/memory-file.md→ "Override files"). - Offer any recommended convergence (optional) — if a note suggests a
/lintpass (e.g. to annotate legacy preference bullets), offer to run it now. It doesn't block. - Advance the marker. Once the required steps (5 and 6) are done, set
onboarded_against: <VERSION>inmemory/meta.mdand append anupdateline tomemory/ledger.md(brain/schemas/capture-rules.md→ "Action ledger") recording the upgrade and the version span migrated. This is what makes the pass run once, not every boot. If the principal defers a required step, leaveonboarded_againstunchanged so the pass re-offers next boot; deferring the optional convergence (step 7) does not block advancing.
If onboarded_against is missing (legacy memory), treat memory as spanning from the earliest release: run the full pass across the whole CHANGELOG.md — in particular detect and run the pre-roster team migration (brain/playbooks/team.md) — before setting onboarded_against: <VERSION>. Don't shortcut to just offering /lint.