ExecPlan board-drift guard
August 6, 2026 · View on GitHub
Keeps a plan's Status: line from lagging its derived state — the case where stored facts say a plan
is done but the markdown still reads Status: In progress, so the work board shows stale work.
Everything here is advisory. Both scripts print; neither mutates a plan nor blocks real work.
Agent-facing summary lives in the
execplan-disciplinewizard profile (rendered intoCLAUDE.md/AGENTS.md). This document is the operator reference: install, wiring, and semantics. Keep the profile short — it is loaded into every session; this file is read when someone is actually wiring the guard up.
Install
bash scripts/setup-drift-guard.sh # install + print config snippets
bash scripts/setup-drift-guard.sh --print-only # print snippets, install nothing
bash scripts/setup-drift-guard.sh --self-test
The installer copies both scripts into ${XDG_DATA_HOME:-$HOME/.local/share}/crux/hooks/ so agent
configs can point at a stable path that survives repo moves, then prints the JSON to merge into
your agent config. It deliberately never edits agent configs itself: merging into an existing hooks
map depends on what else is wired there, and a bad merge is worse than a copy-paste.
The two layers
1. Write-time guard — execplan-status-guard.sh
Runs as a PostToolUse hook on store_fact. When you store a plan-terminal fact — decision:close*,
or a gate:* marked plan-complete — while the plan's leading Status: token is still non-terminal, it
exits 2 and prompts you to flip the line while you still have the context.
Only the leading token counts. Status: In progress (design complete) is non-terminal: the
parenthetical does not make it done.
| Agent | Config file | Hook | Matcher |
|---|---|---|---|
| Claude Code | .claude/settings.json | hooks.PostToolUse | mcp__crux__store_fact |
| codex | .codex/hooks.json | hooks.PostToolUse | store_fact |
2. Boot sweep — reconcile-execplan-status.sh
Run reconcile-execplan-status.sh --quiet on SessionStart in both agents. It GETs
/v1/work?source=all and, for every execplan:* whose derived state is terminal while its leading
Status: token is not, prints one compact line naming the plans to flip.
- Silent when clean.
- Exits 0 and skips gracefully when the daemon is unreachable, so it never blocks a session start.
- Set
CRUX_EXECPLANS_ROOTso it can resolve<slug>.mdfrom a work-item id.
Related
scripts/reconcile-execplan-sessions.sh is a separate detector covering a different drift class:
orphan sessions (a registry entry with no .md) and unparseable plans. It also prints without
mutating. See the workspace-cuecrux profile.
Keeping the daemon's replica current
The drift guard above compares a plan's Status: line against derived state. It
can only compare what the daemon can see, and what it sees is its own replica
of *.md — /v1/work walks CRUX_EXECPLANS_ROOT on every request and
re-projects. Nothing is pushed to it when you edit a plan.
So there are two distinct failure modes, and they look identical from the board:
| symptom | cause |
|---|---|
| plan's state is wrong | its Status: line drifted — the guard above catches this |
| plan is absent | the replica never received the file |
The second is the quiet one. A plan can be committed, pushed, and correct, and still be invisible because the daemon's copy predates it — no error anywhere.
If the deployment has git backing
Set CRUX_EXECPLANS_GIT_REMOTE (+ _BRANCH, _INTERVAL_SECS, _CHECKOUT) and
the replica maintains itself; POST /v1/execplans/refresh pulls on demand. Note
_CHECKOUT is the repository, while the projection root is normally
<checkout>/.agent/execplans — a mismatch is silent, and shows up only as an
empty board.
If it does not
POST /v1/execplans/refresh returns 409 — "git backing is not configured",
which is deliberately distinct from a pull that ran and failed. The replica is
then whatever was last copied there, and it ages silently.
Use scripts/sync-execplans-replica.sh (--dry-run to preview). Run it after
merging plan changes; the board re-projects on the next read, no restart.
Measured on host crux: the replica went four days stale and lost nine
plans, one of them complete, before anyone noticed — and eighteen hours later
another ten files had changed. Treat a manual replica as a thing that stops
happening, and prefer git backing wherever a credential allows it.