Companion to v1_plan.md. That file says what and why. This file says how, who does each phase, and — critically — when the gate between phases closes.
Status: Authored 2026-05-12 during the preparation session that established acture. Phases 0 → 4 specified.
Phase boundaries are gates, not waypoints. Each phase below has:
- Goal — one sentence on what shipping this phase means.
- Scope — exhaustive list of what this phase does and does not include.
- Skills to load — which
acture/.claude/skills/*skills the implementing agent should read first. - Acceptance test — concrete, mechanical checks an agent (or a human) runs to confirm the phase is done.
- Pre-next-phase reflection checklist — what the implementing agent or you (as the planning agent in a future session) must do before the next phase starts. This is where rethinking, refactoring, and design correction happen.
A phase is not done because the acceptance test passes. A phase is done when the acceptance test passes AND the reflection checklist has been worked through. No agent should start Phase N+1 without explicit go-ahead, captured in a paragraph at the end of the phase noting what was learned.
Each phase is sized for a single focused agent session (2–6 hours of agent work). If a phase looks bigger, split it.
Status: ✅ DONE — 2026-05-13. Reflection: docs/phase-0-reflection.md. CI green pending first push.
Goal: acture exists as a publishable TypeScript monorepo. Both names (acture on npm, acture on PyPI) are reserved with v0.0.0 stubs. CI is green. Docs are in place (this is being done during the preparation session).
Done during the preparation session (this session, before Phase 0 begins):
- Migrate docs from wrapex (this session).
- Reserve
actureon npm and PyPI (this session if user authorizes). - Author skills in
.claude/skills/. - Author
AGENTS.md,README.md,wrapex_carryover.md,next_session.md.
Phase 0 agent's job:
- Scaffold a TypeScript monorepo using pnpm workspaces (or npm workspaces — pick one and document the choice).
- Create
packages/core/package with minimalpackage.json(private = false;exports;type: module). - Set up shared
tsconfig.base.jsonand per-packagetsconfig.json. - Set up
tsup(ortshy) for ESM/CJS dual build. - Set up
vitestat the workspace root. - Add
.github/workflows/ci.ymlrunning typecheck + tests on PR. - Smoke test: a
defineCommandexport that returns a frozen object; a vitest unit test that calls it. - Update root
README.mdif it needs polish post-rename. - Verify
npm pack -w packages/coreproduces a valid tarball. - Verify CI green.
acture-architecture-primer(overview of the three primitives)acture-hard-donts(the merge checklist)
# At the acture repo root:
pnpm install # or npm install
pnpm -r typecheck # all packages typecheck
pnpm -r test # all tests pass (only the smoke test exists)
pnpm -w pack packages/core # produces a valid tarball
# CI runs the same and is green on main.Before starting Phase 1, answer these in a markdown note at docs/phase-0-reflection.md:
- Was the monorepo tooling the right choice? pnpm vs npm vs nx vs turborepo — what did you pick, and was it the right call given that acture will have ~10 packages by v1.0?
- Is the
exportsfield per-package agent-friendly? When an agent editspackages/core/src/foo.tsand wants to import from a sibling package in tests, does the path resolve cleanly? - Is the build (
tsup/tshy) producing types and runtime correctly? Runnpm packand inspect the tarball. Does it match whatacture/coreconsumers will receive? - Did anything in Phase 0 surface a hidden assumption in the docs? If yes, fix the doc before Phase 1 starts.
When this reflection note is committed, Phase 0 is done.
Status: ✅ DONE — 2026-05-13. Reflection: docs/phase-1-reflection.md. Acceptance: docs/phase-1-acceptance.md.
Goal: A working acture/core validated end-to-end against a worked example with one consumer adapter. The command record shape, dispatcher signature, when-clause DSL, and state adapter interface are committed.
Packages to ship:
packages/core/(actureon npm) — the core. NO React, NO state library, NO UI deps.packages/state-zustand/(acture-state-zustandon npm; subject to scoping decision) — production reference adapter, ~50 LOC + tests.packages/palette-react/(acture-palette-react) — minimal version, parameter-free commands only.
acture/core surface (Phase 1):
defineCommand<P, R>(spec): CommandRecord<P, R>— type-checked, frozen result. Validates at registration time that param schemas are in the JSON-Schema-representable subset.createRegistry(options?): Registry— owner-scoped disposables;commandsChangedevent;dispatch(id, params, ctx?);get(id);list(); tier-awarelist({ tiers: [...] }).WhenClauseEvaluator— DSL parser/evaluator for:!,&&,||,==,!=,>=,<=,=~,in,not in. Plus a function escape hatch ((ctx) => boolean) flagged in metadata.StateAdapter<S>interface (per research-3 §5):getState,setState(updater),subscribe(listener). PlusPatchCapableAdapter<S>(optional sub-interface) andisPatchCapable<S>type guard.- Schema bridge:
toJsonSchema(record, options?)— accepts injected converter; default uses Zod v4'sz.toJSONSchema. Result<R>type with reserved undo hooks (patches?,effects?). Phase 1 ignores them; they exist so Phase 4+ can add them non-breakingly.
acture-state-zustand surface:
createZustandAdapter<S>(store): PatchCapableAdapter<S>— wrapszustand/vanillacreateStore. Useszustand/middleware/immerwithproduceWithPatchesforsetStateWithPatches.
acture-palette-react surface (Phase 1, minimum):
- Wrap cmdk's
<Command>primitive. - Iterate registry.list() filtering by tier.
- Group by
category. - Show keybinding hints from
record.keybinding. - On selection: if
kind === 'atomic'and params are empty, dispatch immediately. If parameterized: not yet — show "parameterized commands coming in Phase 2" placeholder. - Listen for
commandsChanged.
Worked example: examples/greenfield/graph-editor/
- 6–8 commands:
app.graph.addNode,app.graph.removeNode,app.graph.connectNodes,app.graph.deleteEdge,app.view.zoomToFit,app.selection.selectAll,app.view.toggleGrid(param-free toggle). - All state mutations go through commands (no direct
setState). - State held in a zustand store via the new adapter.
- A
<CommandPalette>(fromacture-palette-react) overlay opens on Ctrl+K and dispatches parameter-free commands. - One parameterized command (
addNode({x, y, label})) is registered but explicitly noted in the README as "Phase 2 will surface this in the palette".
Tests:
- Property-based test (fast-check) for registry invariants: no duplicate IDs; dispatch of unknown ID returns
{ ok: false }; subscribers receivecommandsChangedevents; owner disposal removes owned commands. - Unit tests on the when-clause DSL parser/evaluator.
- Snapshot test:
toJsonSchema(addNodeCommand)matches the expected JSON Schema. - Integration test: the graph editor's state is
JSON.stringify-able and round-trips throughJSON.parse.
acture-architecture-primeracture-command-record-shapeacture-state-adapteracture-schema-bridge(for the toJsonSchema piece)acture-hard-donts(merge checklist)
- The graph editor example runs (
pnpm devinexamples/greenfield/graph-editor/). - The graph editor uses no
setStateoutsideexecutehandlers (verifiable by arg "store.setState" packages/ examples/greenfield/graph-editor/src/ -t tsaudit). - Property tests pass.
- A second agent, given only the API docs at
packages/core/README.md, writes a 7th command (e.g.app.graph.renameNode) and the example accepts it without changes to the registry or palette. Document the test indocs/phase-1-acceptance.mdwith the dialogue trace. JSON.stringify(adapter.getState())round-trips throughJSON.parse(...)and produces an identical state object (deep equality check in a test).- CI green.
Before Phase 2:
- Did the command record shape feel right? Were there fields you wanted to add but resisted (good — that's the closed-surface discipline working: a new field needs a concrete named consumer)? Were there fields you found yourself never using (consider deletion)? Document in
docs/phase-1-reflection.md. - Did the StateAdapter interface stretch to cover the zustand case cleanly? Specifically: did
setState(updater)accept both(s) => Sand(s) => void(Immer-style) cleanly? If you found friction, what would Phase 2 want different before RTK adapter is added? - Was the when-clause DSL parser worth the complexity? Estimate LOC. If it's over 200, ask: is the DSL pulling weight against a
(ctx) => booleaneverywhere? - Did the second-agent test surface any docs gaps? Specifically: did the second agent need to read source instead of docs? Where? Patch docs before Phase 2.
- Any "hard don't" violations creep in? Run the merge checklist from
acture-hard-dontsskill. - Decisions to escalate to user: any irreversible architectural choices made during Phase 1 that the user should ratify before Phase 2 builds on top of them?
Phase 1 is done when the acceptance test passes AND phase-1-reflection.md is committed AND any escalations have user sign-off.
Status: ✅ DONE — 2026-05-13. Reflection: docs/phase-2-reflection.md. All 159 tests pass across 9 packages + 2 worked examples.
Goal: acture is useful across all three positioning paths. Hotkeys, parameterized palette, two state adapters, AI/MCP adapters, two form adapters.
Packages to ship:
packages/hotkeys/— tinykeys integration; plain DOM API + optional React hook.packages/palette-react/(extend) — parameterized command support per research-2 §9: auto-derivedkind, picker chain inside palette forkind: "atomic", modal hand-off (via a form adapter) forkind: "handoff". Thekindderivation is the heuristic from research-2 §9.3.packages/state-redux/— RTK reference adapter.packages/forms-autoform/— Zod-native form adapter.packages/forms-rjsf/— JSON-Schema-native form adapter.packages/mcp/— MCP server adapter; iterates registry, exposes tier-stablecommands as MCP tools by default; tier opt-in for experimental; deprecation banner prepending per research-5 §7.4.packages/ai-vercel/— Vercel AI SDK adapter; same tier rules.
Worked examples added:
examples/drop-in/— mode-1 demo: existing-app skeleton + 5-minute palette + MCP bolt-on.examples/greenfield/graph-editor/extended with: keyboard shortcuts; parameterized commands now work in palette (addNode({x, y, label})); an MCP server runs that exposes the graph commands; an LLM (Claude or GPT-4) demo invokes commands via the Vercel AI adapter.
acture-palette-design(parameterized palette UX)acture-schema-bridge(MCP tool projection)acture-state-adapter(RTK adapter)acture-tier-system(filtering for MCP + AI)acture-hard-donts
- Both worked examples (greenfield + drop-in) run.
- Parameterized commands in the graph editor's palette work as research-2 prescribes:
- 1-param
addNode({label})(afteraddNoderescoped) is collected as a single inline picker step. - 3-param
addNode({x, y, label})with x, y as number inputs hands off to a form by default (auto-derivedkind: "handoff"). - Override demo: same command with explicit
kind: "atomic"shows a 3-step chain instead.
- 1-param
- MCP client (use
@modelcontextprotocol/inspectorCLI) lists the graph editor's commands as tools, calls one, and gets a validResultresponse. - Vercel AI SDK demo: an
npx ts-node examples/greenfield/graph-editor/scripts/ai-demo.tsruns Claude against the graph editor commands and successfully composes a multi-step action ("add three nodes labeled A, B, C and connect them in a triangle"). - Tier filtering: a command marked
@experimentaldoes NOT appear in the default MCPtools/listoutput. The same command DOES appear when the MCP server is created withtiers: ['stable', 'experimental']. - CI green across the new packages.
Before Phase 3:
- Did research-2's auto-derived
kindheuristic match user expectations? Specifically: for each parameterized command in the worked examples, was the auto-derivedkindthe right call, or did you have to override? If override rate > 30%, the heuristic needs refinement. - Did the StateAdapter interface stretch to RTK cleanly? Especially: did the patch-capable interface fit Immer's
produceWithPatchesfor both zustand+immer and RTK's built-in Immer? Document indocs/phase-2-reflection.md. - Did the schema bridge survive the AI SDK contact? Vercel AI SDK accepts Zod directly. MCP wants JSON Schema. Was the conversion path clean, or did edge cases surface? List them.
- What did the keyboard-shortcuts integration teach about the
keybindingfield shape? If you needed an extra field, document why. - Hard-don'ts audit: Did any adapter package start containing business logic? Did core gain a React import?
- Decisions to escalate: anything in Phase 2 that should be ratified before Phase 3 builds the migration package on top?
Phase 2 is done when these are in phase-2-reflection.md and any escalations are resolved.
Status: ✅ DONE — 2026-05-13. Reflection: docs/phase-3-reflection.md. acture-migration ships with 36 unit tests; the examples/migration/zustand-wrap/ before/after pair demonstrates the strangler-fig path end-to-end (6 wraps + 2 graduations, palette overlay, kind-heuristic override rate 0/8 = 0%).
Goal: Ship acture/migration per research-4 §A.6. Ship the migration-track skills. Demonstrate the end-to-end workflow against a small fixture app.
Package to ship:
packages/migration/exporting:wrapMutation<H>(handler: H, options?): H— load-bearing primitive. Wrap an existing function as a command without changing call sites. Options:id?(defaults tohandler.nameor auto-generated),logTo?(defaults to console in dev, noop in prod),onDispatch?.actureMiddleware— Redux/Zustand-compatible store-event interception middleware. Translates store actions into command dispatches without bypassing the registry.chooseImplementation<Args, R>(pick, impls): (...args: Args) => R— 5-line helper. Composes with any feature-flag SDK (LaunchDarkly, Statsig, Unleash, etc.).shadowCompare<Args, R>(modern, legacy, options?)— Scientist-style: run modern, log if legacy result differs.
Skills to ship (in acture/.claude/skills/):
migration-diagnose— rewrite of wrapex01-diagnose.mdagainst acture's API.migration-plan— rewrite of02-plan.md.migration-scaffold— rewrite of03-scaffold.md.migration-wrap— rewrite of04-wrap.mdusingwrapMutation.migration-graduate— NEW. How to retirewrapMutationcalls once the legacy handler is no longer needed. The "let the host die" step in the strangler-fig metaphor.
Worked example:
examples/migration/zustand-wrap/— a small existing zustand React app fixture (NOT an acture-aware app); the implementing agent uses the migration skills to introduce acture, then graduates.
acture-migration-packageacture-architecture-primeracture-hard-donts
- The fixture app at
examples/migration/zustand-wrap/before/is a working React app with no acture imports. - Following the migration skills (
migration-diagnose→migration-plan→migration-scaffold→migration-wrap), at least 5 commands are wrapped without breaking existing UI behavior. - The migrated app (
examples/migration/zustand-wrap/after/) has the same behavior plus a working command palette (Ctrl+K) over the wrapped commands. migration-graduatedemonstrates retiring at least 2 of thewrapMutationcalls (the legacy handlers are now unused).- CI green.
Before Phase 4:
- Was
chooseImplementationactually used in the worked migration? If no — was it because the use case didn't arise, or because the API is awkward? If awkward, redesign before declaring Phase 3 done. (research-4 said this could be the next "drop" candidate if no one uses it.) - Was
shadowCompareused? Same question. - Was
actureMiddlewareused? Did it correctly intercept the store events without bypassing acture'sdispatch? - Did the migration skills produce a workflow Claude Code can follow without human handholding? If you find yourself manually correcting the agent at step N consistently, fix that skill.
- What was the worst trap the migration agent fell into? Document for
migration-diagnoseimprovement. - Hard-don'ts audit.
- Decisions to escalate: anything that needs user ratification before Phase 4 (the stability phase) commits to public-API tier marks?
Phase 3 is done when these are in phase-3-reflection.md.
Status: ✅ DONE — 2026-05-13
Goal: Move from "works" to "production-ready" and ship v1.0.
Tier system enforcement (per research-5 §7):
- Build step that scans
.tssource for@stable/@experimental/@internal/@deprecatedJSDoc tags ondefineCommandcalls and mirrors them into the command'stiermetadata field at build time. (One tsup plugin or one esbuild plugin.) - Runtime gating in
registry.toMCPServer({ tiers }),registry.toAITools({ tiers }),registry.toPaletteCommands({ tiers }). Defaults:['stable']. @deprecatedbanner prepending in description for MCP / AI surfaces.@internalsymbol-token enforcement (per research-5 §7.5): runtime throws if dispatched from outside the registering module.
acture compare-schemas CLI (per research-5 §6):
- New
packages/cli/package with binaryacture. acture compare-schemas <base> [<head>]walks the registry in both refs, projects through schema bridge, diffs tool envelopes (not just inputSchema).--fail-on <severity>for CI gating.--allow-description-edits(per-invocation, NOT a config setting) to downgrade description-only diffs to MINOR.--format json|textoutput. Text default colored, JSON for machines.- Change classifications per research-5 §6.1 table.
packages/devtools/:
- Inspector UI: registry contents, dispatch log, when-clause evaluator state, tier filter preview.
- Use it as a React component embeddable in dev builds.
Hardening:
- Audit all error messages for actionability.
- Add JSDoc to every public export.
- Fill remaining test gaps (target: each
corepublic function has at least one happy-path test + one error-path test). - Bump all packages to v1.0.0.
acture-tier-systemacture-schema-bridgeacture-hard-donts
- A command tagged
@experimentalin source is auto-mirrored totier: 'experimental'at build time (verified by reading the dist.d.ts). registry.toMCPServer()excludes the experimental command fromtools/list.registry.toMCPServer({ tiers: ['stable', 'experimental'] })includes it.- A
@deprecatedcommand's MCP description starts with[DEPRECATED — use X instead]. - An
@internalcommand throws if dispatched from outside its module. acture compare-schemas v0.9.0 HEADcorrectly classifies a removed command as MAJOR.acture compare-schemas v0.9.0 HEAD --allow-description-editsdowngrades description-only changes to MINOR while still flagging structural changes as MAJOR.acture compare-schemas --fail-on majorexits non-zero when MAJOR changes are present (CI gate works).- Devtools inspector renders in the greenfield example.
- v1.0.0 published to npm (dry-run with
npm packfirst).
Before v1.0 release:
- Did the JSDoc-tag-plus-mirror tier system survive the agent-write workflow? Specifically: when an agent edits a command's source, does the build step correctly re-mirror the tag? Did anything drift?
- Was
acture compare-schemasready to gate the v1 release in CI? Perv1_plan.md§7, "expect schedule slip" — was the schedule met? - Are there pending Phase 4 items that should defer to v1.1 instead of blocking v1.0? Document trade-offs.
- Final hard-don'ts audit across the whole codebase.
- Run the second-agent test again at v1.0: a fresh agent reads
acture/AGENTS.mdand the README, then writes a command in a small new app. Does it work end-to-end without reading source?
v1.0 ships when these are answered.
These are decisions where reversing in a later phase costs significant rework. They are listed here so future-phase agents know what NOT to revisit:
- CommandRecord shape (Phase 1). Adding a field after v1.0 is fine (minor); removing or renaming a field is a major break.
- StateAdapter interface (Phase 1). Three methods plus optional capability extension. Adding a method is fine; reshaping is major.
- When-clause DSL grammar (Phase 1). The operator set is locked at v1.0; adding operators is fine; redefining semantics is major.
- Tier metadata field's exact name and enum values (Phase 4).
tier: "stable" | "experimental" | "internal" | "deprecated"— these enum values are part of the public surface andacture compare-schemasreads them. - The
kind: "atomic" | "handoff"enum (Phase 2). Adding a third value (e.g.,"async"or"streaming") post-v1.0 is a minor expansion only if all consumer adapters can handle the unknown value gracefully.
- Macro/replay system: post-v1.
- Telemetry middleware: post-v1.
- Undo subsystem: post-v1; hooks reserved in Phase 1.
- Python companion: post-v1; research-6 not executed.
- Codemods: v1.1 per research-4.
- DOM-event interception: v1.1 per research-4.
Same as v1_plan.md §5 post-v1 list. Repeated here for emphasis: macros, telemetry, sandboxing, Jotai/Valtio adapters, Python.
- Implementing agent: at the start of each phase, read the corresponding section here AND the indicated skills. At the end of each phase, write the reflection note and commit it.
- Planning agent in a future session: start from the most recent
docs/phase-N-reflection.mdanddocs/v1_plan.md. Re-plan only if the reflection surfaced design changes. - User: the reflection notes are your check-in points. After each phase's reflection note is committed, you can audit the decision trail without re-reading source.