planr 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,49 +22,6 @@ Flat todo lists break down the moment real work has structure. Planr models work
22
22
 
23
23
  Three layers make that work: **Plans** (reviewable Markdown packages), the **Map** (live dependency graph with picks, reviews, logs), and **Agent loops** (skills, CLI, and MCP workflows for every major coding agent). Full model: [Task Graph Model](docs/TASK_GRAPH_MODEL.md) and [Operating Model](docs/OPERATING_MODEL.md).
24
24
 
25
- ## New in 1.3.0: Native Host Hooks
26
-
27
- `planr install codex|claude|cursor` now wires Planr into the host's native hook system by default — every new session (including post-compaction restarts) gets one compact state block injected automatically:
28
-
29
- ```text
30
- ## planr state
31
- project: Hookboard | map: 5/5 settled | 0 ready, 0 picked, 0 in_review
32
- goal contract: DONE when every in-scope map item is closed with log evidence, ...
33
- routing: registry active (3 profiles; pick packets carry model routing)
34
- next: planr plan audit pln-fc584c28 --json
35
- ```
36
-
37
- - **`planr prime`** — the state block behind the hooks: project, map counts, held items with log status, goal contract, and the next command. Silent in repos without a Planr database.
38
- - **Loop recovery becomes mechanism, not discipline** — an agent that restarts or compacts mid-loop picks up exactly where the map says it left off.
39
- - **Evidence guard (Cursor)** — a subagent that stops while its own pick has no completion log gets one advisory reminder naming the item and the two ways out.
40
- - **Fail-open and additive** — hooks never block a session (10s timeout, always exit 0), existing hook files are merged, `--no-hooks` opts out.
41
-
42
- Full guide: [Hooks](docs/HOOKS.md) · [Release notes](https://github.com/instructa/planr/releases/tag/v1.3.0).
43
-
44
- ## Model Routing (1.2.0)
45
-
46
- Declare once which model handles which work — every task then carries its own routing, and your agents delegate automatically:
47
-
48
- ```toml
49
- # .planr/agents.toml (write it with `planr agents init`)
50
- [profiles.frontender]
51
- client = "cursor"
52
- model = "opus"
53
- skill = "frontend-design"
54
-
55
- [[routes]]
56
- match = { work_type = "frontend" }
57
- profile = "frontender"
58
- fallbacks = ["driver"]
59
- ```
60
-
61
- - **Routing travels in the pick packet** — `planr pick --json` hands the worker its profile, model, and paired skill; `planr pick --peek` lets dispatching drivers read it without taking the lease.
62
- - **Rendered into your hosts' native config** — `planr install codex|claude|cursor` writes the subagent role files with model pins from the registry, in each host's exact vocabulary.
63
- - **Declared vs. actual, with receipts** — workers report the profile they ran on, runs record the observed host, and `planr trace item` shows deviations as advisory markers.
64
- - **Use-case pools** — free-form work types (`frontend`, `backend`, ...) declared right in the plan's task list (`### TASK-001 (backend): ...`), plus per-item pins via `planr item route`.
65
-
66
- Routing is advisory by design: Planr never dispatches models and never blocks a pick — hosts stay the authority. Full guide: [Model Routing](docs/MODEL_ROUTING.md) · replayable walkthrough: [Worked Example: Web App](docs/EXAMPLE_WEBAPP.md) · [Release notes](https://github.com/instructa/planr/releases/tag/v1.2.0).
67
-
68
25
  ## Install
69
26
 
70
27
  ```bash
@@ -93,7 +50,7 @@ Manual downloads, from-source builds, and client wiring details: [Install Guide]
93
50
 
94
51
  ## Install The Plugin (Skills)
95
52
 
96
- The plugin under `plugins/planr` carries the nine Planr skills plus the `planr-worker` and `planr-reviewer` subagent roles. The `planr` CLI (above) is required separately.
53
+ The plugin under `plugins/planr` carries the ten Planr workflow skills. Optional model-routing roles come from repository-local routing bundles. The `planr` CLI (above) is required separately.
97
54
 
98
55
  <a id="install-plugin-codex"></a>
99
56
  <details>
@@ -174,6 +131,14 @@ the feature working in the browser. Iteration budget: 10.
174
131
 
175
132
  Mid-project work (a new feature, refactor, or fix on an existing project) works the same — it gets its own feature-scoped plan and extends the existing map. Both journeys with example prompts: [Two Journeys](docs/SKILLS.md#two-journeys-new-project-vs-existing-project). Watch progress anytime with `planr map show`.
176
133
 
134
+ ## What's new
135
+
136
+ - **1.5.0 — Optional routing policies:** Planr Core stays provider-neutral while the independently buildable `planr-routing` package owns model policies and host bindings for Codex, Claude Code, Cursor, and mixed-host setups. Start with [Routing Bundles](docs/ROUTING_BUNDLES.md), the [`planr-routing` guide](planr-routing/docs/MODEL_ROUTING_POLICY.md), and the [1.5.0 release notes](https://github.com/instructa/planr/releases/tag/v1.5.0).
137
+ - **1.4.0 — Verified presets:** Added policy-driven composition, evaluation, signed registry evidence, and the public catalog. See the [1.4.0 release notes](https://github.com/instructa/planr/releases/tag/v1.4.0).
138
+ - **1.3.0 — Native host hooks:** Added automatic session-state injection and loop recovery for supported hosts. See the [Hooks guide](docs/HOOKS.md) and [1.3.0 release notes](https://github.com/instructa/planr/releases/tag/v1.3.0).
139
+
140
+ For the complete release history, see the [Changelog](CHANGELOG.md).
141
+
177
142
  ## Docs
178
143
 
179
144
  - [Install](docs/INSTALL.md)
@@ -6,7 +6,7 @@ Planr V1 is a single Rust binary with explicit module ownership. The crate stays
6
6
 
7
7
  - `src/`: the Rust CLI (module ownership below).
8
8
  - `tests/e2e.rs`: real CLI, MCP, HTTP, import, review-gate, run-log, and concurrent-pick tests.
9
- - `plugins/planr/`: the installable plugin payload — all nine skills, the worker and reviewer subagent roles, and the per-host plugin manifests.
9
+ - `plugins/planr/`: the installable plugin payload — all ten skills, independent Claude/Cursor worker and reviewer role assets, and the per-host plugin manifests. Native Codex role TOMLs are generated from the selected binding instead of shipped here.
10
10
  - `.agents/plugins/marketplace.json`, `.claude-plugin/marketplace.json`: marketplace manifests pointing Codex and Claude Code at `plugins/planr`.
11
11
  - `docs/`: user and contributor guides; `docs/planr-spec/` is the production specification package for Planr V1.
12
12
  - `examples/real-world-flow.md`: executable real-world operator flow.
@@ -40,10 +40,15 @@ Planr V1 is a single Rust binary with explicit module ownership. The crate stays
40
40
  - `src/storage/rows.rs`: SQLite row mapping boundary. Owns row-to-DTO and row-to-JSON mapping functions.
41
41
  - `src/planpack.rs`: Markdown package generation and parsing. Owns project context templates, product/build plan templates, plan metadata parsing, hashes, search body extraction, and task extraction.
42
42
  - `src/agents.rs`: agent profile registry core. Owns `.planr/agents.toml` parsing, registry validation warnings, and the pure advisory `resolve_route` precedence logic (override > work_type > plan > default); no storage or host concerns.
43
+ - `src/usage_policy.rs`: provider-neutral Usage Policy v1 core. Owns strict `.planr/policy.toml` parsing, policy and task-contract vocabulary, materiality classification, budget/concurrency validation, and the pure five-way transition resolver; it contains no provider ids, host dispatch, or execution-permission behavior.
44
+ - `src/execution_policy.rs`: execution admission core. Owns per-role filesystem, network, tool/MCP, structured command, environment, hook, secret, and approval grants; permission-diff previews; bounded task-contract admission; fail-closed command grammar; and isolated write-scope concurrency. It never selects models or mutates graph state. `src/app/policy.rs` binds that pure decision to the current SQLite lease owner and pick token, and only treats an admission from that exact lease generation as authoritative.
45
+ - `src/route_audit.rs`: provider-neutral run-observation contract. Owns strict requested/resolved/effective route stages, model/effort/fork enforcement confidence, transition provenance, policy/binding versions, and per-dimension metering confidence. It rejects requested-only values in the effective stage rather than inferring host execution.
46
+ - `src/routing_bundle.rs`: provider-neutral RoutingBundle v1 contract and repository transaction boundary. Owns validation, hashes, signatures, allowlisted paths, conflict and symlink rejection, atomic apply, rollback, and application receipts.
47
+ - `planr-routing/`: independently buildable workspace package that owns model policies, host bindings, generated host artifacts, probes, evaluation, signing, registry data, and catalog publication.
43
48
  - `src/app/agents.rs`: routing application boundary. Owns the `agents` and `item route` command handlers, the shared `*_value` JSON shapes reused by MCP, per-item route facts assembly, and registry-aware role content selection for installs.
44
49
  - `src/app/agents_init.rs`: registry bootstrap boundary. Owns `planr agents init` — the static cost-tiering scaffold and, per the agent-pool plan, the flag-spec builder and interactive wizard.
45
50
  - `src/integrations.rs`: agent-client integration descriptor boundary. Owns Codex, Claude Code, Cursor, MCP install metadata, MCP tool schemas, MCP resources, and MCP text response wrapping.
46
- - `src/rolefiles.rs`: host role file boundary. Owns the shipped static subagent role files and Cursor skills payloads, plus the pure renderers that re-pin role files from registry profiles (Codex TOML with strict field names, Claude/Cursor markdown frontmatter) under the generated-from header.
51
+ - `src/rolefiles.rs`: static host workflow roles and Cursor skill payloads. It does not select or pin models; generated routing artifacts belong to `planr-routing` bundles.
47
52
  - `src/util.rs`: small CLI-boundary utilities. Owns ids, timestamps, path helpers, output formatting, and safe file writes.
48
53
 
49
54
  ## Boundary Rules
@@ -49,7 +49,8 @@ planr artifact show <artifact-id>
49
49
  planr artifact list [--item <item-id>]
50
50
  planr event list [--item <item-id>] [--limit 50]
51
51
  planr debug bundle [--item <item-id>] --preview
52
- planr log add --item <item-id> --summary "..." [--files a --files b | --files a,b] [--cmd "..."] [--kind completion|progress|verification] [--profile <id>]
52
+ planr trace item <item-id>
53
+ planr log add --item <item-id> --summary "..." [--files a --files b | --files a,b] [--cmd "..."] [--kind completion|progress|verification] [--profile <id>] [--route-audit <observation.json>]
53
54
  # machine consumers: --cmd writes the `commands` field in log JSON; --tests writes `tests`
54
55
  planr review request <item-id>
55
56
  planr review annotate <item-id> --message "..." [--severity info|warning|blocking] [--file path] [--line N] [--author "..."]
@@ -66,6 +67,9 @@ planr agents init [--force]
66
67
  planr agents init --profile|--skill|--route|--default-route|--interactive
67
68
  planr agents list [--json]
68
69
  planr agents check
70
+ planr routing bundle inspect <bundle.json>
71
+ planr routing bundle preview <bundle.json>
72
+ planr routing bundle apply <bundle.json>
69
73
  planr doctor [--client codex|claude|cursor|all]
70
74
  planr install codex|claude|cursor [--dry-run] [--no-mcp] [--force] [--no-hooks]
71
75
  planr prompt cli|mcp|http [--client codex|claude|cursor|all]
@@ -123,7 +127,7 @@ With `--json`, responses follow one convention so agents never guess where data
123
127
 
124
128
  `review close` writes `.planr/reviews/<review-item-id>.review.md` and registers it as a review artifact. A `not-complete` or `unclear` verdict creates fix and follow-up review work; the follow-up review gates the same target item, so the chain keeps working with `--close-target`. With `--close-target` (complete verdicts only) the reviewed item is closed in the same command, provided it already has a completion log; the artifact is rendered after the target transition, so it snapshots the final target status. `--close-target` is also available through MCP `planr_review_close` and HTTP `POST /v1/reviews/{id}/close` (`"close_target": true`). `review close` responses include the same `remaining` progress snapshot as `done` and `close`. `--reviewer <id>` records the checker's identity on the review log, artifact, and event (defaults to the worker id), keeping maker and checker distinguishable in the audit trail. Closing an already-settled review fails with error code `already_closed` instead of silently duplicating evidence logs. The maker/checker split is derived, not declared: `review_mode` compares the closing reviewer identity against the target item's lease holder and reports `single_agent`, `independent`, or `unattributed` in the response, review log, artifact, and event — no ceremony note required. An `unattributed` close explains itself in the output: it means the target has no recorded lease (work was never picked or its lease was released), not that the reviewer's identity was missing.
125
129
 
126
- `trace item` on a review item inlines the target item and its evidence logs under `target`, so a reviewer's first trace already contains what is being audited. The human (non-JSON) mode renders the packet: status, owner, links, logs. When the item has a declared route or a run recorded a profile, the trace gains a `routing` section — the declared route next to every run's actual client/profile with an advisory `mismatch` marker and a mismatch count; items without either keep the exact pre-routing trace shape.
130
+ `trace item` on a review item inlines the target item and its evidence logs under `target`, so a reviewer's first trace already contains what is being audited. The human (non-JSON) mode renders the packet: status, owner, links, logs. When the item has a declared route, a run profile, or a route observation, the trace gains a `routing` section — the declared route next to every run's actual client/profile and, when supplied, its independent requested → resolved → effective model/effort/context-fork stages, transition reason, policy/binding provenance, and per-dimension metering confidence. Unknown effective host values remain `null`/`unavailable`; requested values are never copied forward as proof. MCP `planr_trace_item` returns the identical JSON shape.
127
131
 
128
132
  `doctor` also reports the agent registry in all states without ever failing: absent (informational hint), degraded (parse error with line context), or loaded with profile/route counts, validation warnings, and per-artifact drift — a rendered role file whose content no longer matches what the current registry would render is flagged `drifted` with a `planr install <client> --force` hint, while files without the generated-from header are the user's (`manual`) and never flagged.
129
133
 
@@ -135,9 +139,13 @@ With `--json`, responses follow one convention so agents never guess where data
135
139
 
136
140
  `pick --work-type <type>` restricts the lease to one work type, so checker agents pick only `review` items and makers only work items. `pick --plan <plan-id>` restricts the lease to one plan's items, so plan-scoped goal runs never pick work outside their contract even when other plans share the board; an unknown plan id is an error, never a silent unscoped pick. Both filters are available on MCP `planr_pick_item` and HTTP `POST /v1/pick` (`work_type`, `plan`). A null pick is never blind: `{"item": null}` carries a `reason` (`empty_map`, `all_settled`, `nothing_ready`, `ready_items_excluded_by_filter`) and the `remaining` snapshot. When ready work exists but the active filters rejected all of it, `excluded` lists each ready item with the cause (`work_type` mismatch, outside the `--plan` scope, or just requested by this worker) and `repair` carries the exact pick commands that would lease that work — across CLI, MCP, and HTTP. On a review item, `close_effect` previews the full `--close-target` cascade: it lists the work that closing the review (and with it the reviewed item) would unlock.
137
141
 
138
- `agents init` writes a commented starter registry with the cost-tiering defaults (premium driver that keeps the verdicts, standard steerable implementer, budget helper for token-hungry side work, plus code/fix/review/default routes) and names the follow-up commands; an existing `.planr/agents.toml` is never overwritten without `--force`. Individual pools generate from repeatable spec flags `--profile designer=claude-code/opus@high#premium` declares a profile, `--skill designer=frontend-design` pairs it with a skill, `--route frontend=designer,driver` routes a use-case work type with fallbacks, `--default-route` catches the rest; validation is fail-closed (unknown profile references and malformed specs error, naming the grammar, before anything is written), and consistent specs generate a zero-warning registry. `--interactive` walks the same questions as guided prompts (requires a terminal; conflicts with spec flags) and can render the host role files at the end. `agents list` and `agents check` inspect the agent profile registry (`.planr/agents.toml`): named profiles (host client, model, effort, cost tier) and advisory routes from work selectors to profiles. When a registry resolves for a picked item, the pick packet carries a `routing` block — profile, client, model, effort, cost tier, fallback chain, and the matched selector — resolved with precedence per-item override > `work_type` > `plan` > default route. `item route` shows an item's resolved route and its source (`override` or `policy`), pins a registry profile with `--set <profile>` (emits `route_overridden`), and unpins with `--clear` (emits `route_override_cleared`); a pinned profile that later disappears from the registry falls back to policy routing with a repair hint. The same JSON shapes are exposed over MCP: `planr_agents_list` and `planr_item_route` read, `planr_item_route_set` and `planr_item_route_clear` mutate. Routing is advisory: Planr never dispatches models, a missing registry just omits the block, a malformed one degrades picking to no-routing (only `agents check` fails, with the parser's line context), and validation warnings (unknown profile references, duplicate selectors, review work on a budget tier, secret-like values) never affect exit codes. When a registry is present, `install codex|claude|cursor` renders the subagent role files with model pins from it — the `work_type=code` route pins the worker, the `work_type=review` route pins the reviewer, and a role only pins profiles whose `client` matches the install target (a Cursor-profile review route never writes a Cursor model into a Codex TOML). Rendered files carry a `# generated from .planr/agents.toml` header; without a registry (or when no matching route resolves) the shipped static role files are written byte-identically, and existing files are never overwritten without `install --force`. Full guide: [Model Routing](MODEL_ROUTING.md).
142
+ `policy show` and `policy check` inspect the provider-neutral Usage Policy v1 file (`.planr/policy.toml`). A missing policy is an explicit successful state that preserves existing advisory routing; a malformed or unsafe policy fails `check` with parser/field diagnostics and leaves enforcement unavailable. Usage Policy v1 fixes delegation depth at one and keeps retry, availability fallback, quality escalation, quota downgrade, and safety stop as distinct transition contracts. Its separate `execution` section declares bounded per-role filesystem, network, tool/MCP, structured command, environment, hook, secret-reference, and approval grants. `policy admit <request.json>` requires the picked item's current `item_id` and `pick_token`, authorizes the current worker as lease owner, evaluates a bounded task contract before delegation, previews any permission addition, records the decision and lease generation, rejects unclassified commands and destructive or out-of-scope work, permits overlapping readers, and admits concurrent writers only for disjoint scopes isolated in distinct worktrees. Released or recovered leases never inherit historical admitted scopes. MCP `planr_policy_show`, `planr_policy_check`, and `planr_policy_admit`, plus HTTP `POST /v1/policy/admit`, call the same admission service and return the same decision shape.
139
143
 
140
- `log add` and `done` accept `--profile <id>` the registry profile the run actually executed on (`PLANR_PROFILE` env is the fallback, so rendered role files can export it). The profile is stored on the recorded run; when it differs from the item's declared route, Planr emits an advisory `route_mismatch_observed` event (declared, actual, run id) visible in `event list --item`. Runs without a profile, logs without commands/tests, and projects without a registry produce no comparison and no event, and mismatches never block logging, review, or closure. The MCP `planr_log_add` tool and `POST .../log` accept the same optional `profile`.
144
+ `routing bundle inspect|preview|apply` is Planr Core's only policy-package boundary. It accepts RoutingBundle v1, validates payload hashes, restricts targets to allowlisted repository-local paths, rejects traversal, symlinks, parent/child collisions, conflicts, and unsupported versions, and applies the validated set atomically. Signed bundles require both `--trusted-signer` and an external `--trusted-public-key-file`; a key embedded beside the content is never trusted. It never edits user configuration. Named policies, model ids, host bindings, evaluation, signing, and catalog operations are provided by the optional `planr-routing` package. See [Routing Bundles](ROUTING_BUNDLES.md).
145
+
146
+ `agents init` writes a provider-neutral `.planr/agents.toml` scaffold or compiles explicit profile and route flags. `agents list` and `agents check` inspect it, while pick packets carry the resolved opaque profile, host, model, effort, tier, skill, route chain, and matched selector. Full guide: [Model Routing](MODEL_ROUTING.md).
147
+
148
+ `log add` and `done` accept `--profile <id>` — the registry profile the run actually executed on (`PLANR_PROFILE` env is the fallback, so rendered role files can export it). They also accept `--route-audit <observation.json>` for the strict three-stage observation contract. A route audit records each stage's model, effort, and context-fork value with enforcement state (`verified`, `requested_only`, `estimated`, or `unavailable`), an evidence-source enum, typed transition plus reason, policy/binding ids and versions, and wall-time/tool/token/credit values with independent confidence. It is stored in run metadata, projected into its durable log and trace, and emits local route-stage/transition events. The profile remains backward compatible: when it differs from the item's declared route, Planr emits advisory `route_mismatch_observed`. Runs without commands/tests record neither a run nor a route observation. MCP `planr_log_add` and HTTP item log accept the same optional `route_observation` object.
141
149
 
142
150
  `artifact add` infers the mime type from the file extension when `--path` is given without `--mime` (PNG screenshots land as `image/png`, not `text/plain`); inline `--content` defaults to `text/plain`. The same inference applies on MCP `planr_artifact_add` and HTTP `POST /v1/artifacts`.
143
151
 
@@ -147,6 +155,6 @@ With `--json`, responses follow one convention so agents never guess where data
147
155
 
148
156
  `serve` exposes the local review workspace at `/review` and its JSON projection at `/v1/review-workspace`.
149
157
 
150
- `prompt` prints ready-to-use agent instructions without editing global config. Use `prompt cli` for shell agents, `prompt mcp` for MCP setup text, and `prompt http` for localhost automation/review workspace usage. `prompt routing` emits a paste-ready model-prioritization block for the driver session: the route table from `.planr/agents.toml` (every route, profile, and fallback), per-host dispatch guidance with the known traps (Codex `fork_turns: "none"` and session-restart requirement, the Claude `CLAUDE_CODE_SUBAGENT_MODEL` env preemption, Cursor's silent plan/policy/Max-Mode overrides), and process-dispatch snippets (`codex exec`, `pi`, `opencode run`) for hosts without role files; `--json` carries the same content structured, and a missing or unreadable registry still prints the host guidance with a pointer instead of failing.
158
+ `prompt` prints ready-to-use agent instructions without editing global config. Use `prompt cli` for shell agents, `prompt mcp` for MCP setup text, and `prompt http` for localhost automation/review workspace usage. `prompt routing` emits a paste-ready model-prioritization block for the driver session: the route table from `.planr/agents.toml` (every route, profile, and fallback), native Codex `agent_type` plus explicit fork guidance referring to the generated repository role TOMLs, the Claude `CLAUDE_CODE_SUBAGENT_MODEL` env preemption, Cursor's silent plan/policy/Max-Mode overrides, and process-dispatch snippets (`pi`, `opencode run`) only for hosts without role files. `--json` carries the same content structured, and a missing or unreadable registry still prints the host guidance with a pointer instead of failing.
151
159
 
152
160
  `export` writes a reusable Planr JSON package with package requirements metadata, graph state, contexts, optional logs, optional plan file snapshots, review artifact snapshots, and — when present — the agent registry (`.planr/agents.toml`) as a raw snapshot. `import` previews JSON packages by default and mutates only with `--confirm`; the preview names the registry with its exact action (`create`, `identical`, or `conflict`), and an existing registry that differs is never overwritten — the conflict is reported with a hint to remove the local file first. Packages without a registry import unchanged.
package/docs/CODEX.md CHANGED
@@ -22,15 +22,15 @@ $planr-goal <your goal>
22
22
 
23
23
  The stop condition lives in Planr (`--tag goal-contract`), so a dead session resumes with the same starter line from zero chat context.
24
24
 
25
- Run the driver session on your strongest tier (e.g. `gpt-5.5` at `model_reasoning_effort = "high"` in `~/.codex/config.toml`). The provisioned worker role pins a cheaper tier; the reviewer deliberately inherits the session model:
25
+ Optionally compile and apply a repository-local Codex routing bundle before starting the driver:
26
26
 
27
- ```toml
28
- # .codex/agents/planr-worker.toml
29
- model = "gpt-5.5"
30
- model_reasoning_effort = "medium"
27
+ ```bash
28
+ planr-routing compile balanced --host codex-openai --output routing-bundle.json
29
+ planr routing bundle preview routing-bundle.json
30
+ planr routing bundle apply routing-bundle.json
31
31
  ```
32
32
 
33
- Verify the pin once: some Codex versions ignore custom agent files on spawn ([openai/codex#26868](https://github.com/openai/codex/issues/26868)) and the child silently inherits the parent model. Spawn `planr_worker` on a trivial item and confirm the child metadata shows the pinned model and effort with a non-null `agent_path`. Full workflow, recovery, per-host variants, and the tiering rationale: [Long-Running Goals](GOALS.md).
33
+ The bundle generates repository-local roles and a routing skill. Follow that generated skill exactly, restart after role changes, and confirm child metadata reports the expected model, effort, role path, and context-fork behavior. Full workflow: [Long-Running Goals](GOALS.md).
34
34
 
35
35
  ## MCP
36
36
 
@@ -1,138 +1,104 @@
1
1
  # Worked Example: Routing a Small Web App
2
2
 
3
- A complete, replayable walkthrough of [model routing](MODEL_ROUTING.md) on a real shape of work: a todo web app with a frontend and a backend, implemented by different models. Every output below is from an actual run copy the commands into an empty directory and you get the same results.
3
+ This provider-neutral example routes a todo web app by work type. Planr Core treats every profile field as opaque data and does not select a host or model.
4
4
 
5
- The pool we want:
6
-
7
- | Use case | Who runs it | Paired skill |
8
- | --- | --- | --- |
9
- | planning + review | Fable on Cursor (the driver session) | — |
10
- | frontend, design | Opus on Claude Code | `frontend-design` |
11
- | backend | GPT-5.5 on Codex | `planr-work` |
12
-
13
- ## Step 1: Project and pool — two commands, once
5
+ ## 1. Create the project and registry
14
6
 
15
7
  ```bash
16
8
  planr project init "Todo Webapp"
17
-
18
- planr agents init \
19
- --profile driver=cursor/fable-5@high#premium \
20
- --profile frontender=claude-code/opus@high#premium \
21
- --profile backender=codex/gpt-5.5@xhigh#standard \
22
- --skill frontender=frontend-design \
23
- --skill backender=planr-work \
24
- --route frontend=frontender,driver \
25
- --route backend=backender,driver \
26
- --route review=driver \
27
- --default-route backender,driver
28
-
29
- planr agents check
9
+ planr agents init
30
10
  ```
31
11
 
32
- ```text
33
- wrote .planr/agents.toml (generated from the flag specs)
34
- agent registry check passed
12
+ Edit the repository-local `.planr/agents.toml` with profiles owned by your team or routing package:
13
+
14
+ ```toml
15
+ [profiles.frontend-worker]
16
+ client = "your-host"
17
+ model = "your-fast-model"
18
+ effort = "medium"
19
+ cost_tier = "budget"
20
+ skill = "planr-work"
21
+
22
+ [profiles.backend-worker]
23
+ client = "your-host"
24
+ model = "your-engineering-model"
25
+ effort = "high"
26
+ cost_tier = "standard"
27
+ skill = "planr-work"
28
+
29
+ [profiles.independent-reviewer]
30
+ client = "your-host"
31
+ model = "your-review-model"
32
+ effort = "high"
33
+ cost_tier = "premium"
34
+ skill = "planr-review"
35
+
36
+ [[routes]]
37
+ match = { work_type = "frontend" }
38
+ profile = "frontend-worker"
39
+
40
+ [[routes]]
41
+ match = { work_type = "backend" }
42
+ profile = "backend-worker"
43
+
44
+ [[routes]]
45
+ match = { work_type = "review" }
46
+ profile = "independent-reviewer"
35
47
  ```
36
48
 
37
- Prefer questions over flags? `planr agents init --interactive` walks the same inputs as guided prompts and can render the host role files at the end. Either way, validation is fail-closed: a typo in a profile reference errors before anything is written.
49
+ Validate without contacting a provider:
50
+
51
+ ```bash
52
+ planr agents check
53
+ planr agents list
54
+ ```
38
55
 
39
- Note what the routes encode: review work is pinned to the premium driver (verdicts stay strong), everything else runs on the cheaper tier its use case calls for, and every route falls back to the driver when the primary hits a rate limit.
56
+ Alternatively, an external policy package can compile a strict RoutingBundle v1. Preview and apply it through the generic Core boundary:
40
57
 
41
- ## Step 2: Items carry the use case as their work type
58
+ ```bash
59
+ planr routing bundle inspect routing-bundle.json
60
+ planr routing bundle preview routing-bundle.json
61
+ planr routing bundle apply routing-bundle.json
62
+ ```
42
63
 
43
- Work types are free-form, so the use case is just `--work-type`:
64
+ ## 2. Tag items by use case
44
65
 
45
66
  ```bash
46
67
  planr item create "API: task CRUD endpoints" \
47
- --description "Express routes GET/POST/PATCH/DELETE /api/tasks with sqlite store" \
68
+ --description "GET/POST/PATCH/DELETE /api/tasks with sqlite store" \
48
69
  --work-type backend
49
70
 
50
71
  planr item create "UI: task list and add form" \
51
- --description "React list view + optimistic add form against /api/tasks" \
52
- --work-type frontend
53
-
54
- planr item create "UI: design pass on empty and error states" \
55
- --description "Empty state, loading skeleton, error toast" \
72
+ --description "List view and optimistic add form" \
56
73
  --work-type frontend
57
74
  ```
58
75
 
59
- ## Step 3: The pick decides who works
60
-
61
- Same board, two different answers. The backend pull:
76
+ ## 3. Pick and dispatch
62
77
 
63
78
  ```bash
64
79
  planr pick --work-type backend --json
65
80
  ```
66
81
 
67
- ```json
68
- "routing": {
69
- "profile": "backender",
70
- "client": "codex",
71
- "model": "gpt-5.5",
72
- "effort": "xhigh",
73
- "cost_tier": "standard",
74
- "skill": "planr-work",
75
- "fallbacks": ["driver"],
76
- "matched_selector": "work_type=backend"
77
- }
78
- ```
79
-
80
- And the frontend pull:
81
-
82
- ```bash
83
- planr pick --work-type frontend --json
84
- ```
85
-
86
- ```json
87
- "routing": {
88
- "profile": "frontender",
89
- "client": "claude-code",
90
- "model": "opus",
91
- "skill": "frontend-design",
92
- "fallbacks": ["driver"],
93
- "matched_selector": "work_type=frontend"
94
- }
95
- ```
82
+ The pick packet contains the selected opaque profile, client, model, effort, skill, and selector. The host remains responsible for dispatching it; Planr never claims that the requested model actually ran.
96
83
 
97
- The driver session dispatches from the packet alone: the backend item goes to Codex (`codex exec --model gpt-5.5 -c model_reasoning_effort="xhigh" ...` — `planr prompt routing` prints this snippet pre-filled), the frontend item to a Claude Code subagent dispatched with its paired skill (`Use $frontend-design on item <id>`). Two workers can pull in parallel — each `pick --work-type <use-case>` takes its own lane, and the pick lease keeps one owner per item.
98
-
99
- ## Step 4: Workers close with evidence and their profile
84
+ ## 4. Close with evidence
100
85
 
101
86
  ```bash
102
87
  planr done <item-id> \
103
- --summary "CRUD endpoints implemented; 8 supertest cases green" \
88
+ --summary "CRUD endpoints implemented; 8 tests green" \
104
89
  --files server/routes/tasks.ts \
105
90
  --tests "vitest --run: 8 passed" \
106
- --profile backender
91
+ --profile backend-worker
107
92
  ```
108
93
 
109
- `--profile` (or the `PLANR_PROFILE` env var) records which registry profile the run actually executed on. It is part of the evidence, not a formality the next step is why.
110
-
111
- ## Step 5: The trace proves the plan was followed
94
+ `--profile` records the run's claimed profile. For stronger evidence, attach a route observation that keeps requested, host-resolved, and effective values separate.
112
95
 
113
- Every host has a silent override path (env clamps, admin policies, fork semantics — see the [host matrix](MODEL_ROUTING.md#host-matrix)), so a pin alone is not proof. In this run, the backend item was closed on the declared profile and the frontend item deliberately on the wrong one:
96
+ ## 5. Trace declared versus actual
114
97
 
115
98
  ```bash
116
- planr trace item <backend-item>
117
- planr trace item <frontend-item>
99
+ planr trace item <item-id>
118
100
  ```
119
101
 
120
- ```text
121
- routing declared: backender (work_type=backend)
122
- run run-5552fad6 profile backender
123
-
124
- routing declared: frontender (work_type=frontend)
125
- run run-bc0994cb profile driver (differs from declared route; advisory)
126
- ```
127
-
128
- The mismatch also lands as a `route_mismatch_observed` event (`planr event list`). Advisory by design: nothing blocked, but the drift is on record instead of invisible.
129
-
130
- ## The whole model in one line each
131
-
132
- 1. **Declare** the pool once: `planr agents init` (flags, wizard, or the plain scaffold).
133
- 2. **Tag** items with their use case: `--work-type frontend|backend|...` — free-form.
134
- 3. **Pick** routes: every packet carries profile, model, skill, and fallbacks.
135
- 4. **Deliver** with `--profile` as part of the evidence.
136
- 5. **Trace** proves declared vs. actual — silent overrides get caught.
102
+ A mismatch is recorded as advisory evidence instead of silently rewritten. Missing effective-host evidence stays unavailable and is never inferred from the registry.
137
103
 
138
- One-off exceptions never need a registry edit: `planr item route <id> --set driver` pins a single gnarly item to the premium tier and `--clear` restores policy. Details and failure behavior: [Model Routing](MODEL_ROUTING.md); the cost logic behind the tiers: [GOALS.md Cost Tiering](GOALS.md#cost-tiering).
104
+ One-off exceptions use `planr item route <id> --set <profile>`; `--clear` restores policy resolution. See [Model Routing](MODEL_ROUTING.md) and [Routing Bundles](ROUTING_BUNDLES.md).
package/docs/GOALS.md CHANGED
@@ -87,12 +87,15 @@ Iteration 1 reads the map and the stored contract: items already settled stay se
87
87
 
88
88
  ### Codex with `/goal`
89
89
 
90
- The recommended combination. Install the plugin and provision the subagent roles once:
90
+ Install the plugin, initialize the repository, and optionally apply a repository routing bundle:
91
91
 
92
92
  ```bash
93
93
  codex plugin marketplace add instructa/planr
94
94
  codex plugin add planr@planr
95
- planr project init "My Product" --client codex # writes .codex/agents/planr-worker.toml + planr-reviewer.toml
95
+ planr project init "My Product"
96
+ planr-routing compile balanced --host codex-openai --output routing-bundle.json
97
+ planr routing bundle preview routing-bundle.json
98
+ planr routing bundle apply routing-bundle.json
96
99
  ```
97
100
 
98
101
  Then:
@@ -102,7 +105,7 @@ $planr-goal <your goal> # prep: plan, map, contract, starter command
102
105
  /goal Use $planr-loop on plan <plan-id>. The loop contract is stored in planr context (tag: goal-contract).
103
106
  ```
104
107
 
105
- The `/goal` PM dispatches `spawn the planr_worker agent for item <id>` and `spawn the planr_reviewer agent for item <id>` the role files preload `$planr-work` and `$planr-review`, so dispatches stay one line. Codex Automations work the same way: set the automation prompt to the starter line. The provisioned worker role pins a cheaper effort tier; see [Cost Tiering](#cost-tiering).
108
+ When a routing bundle is applied, the `/goal` PM follows its generated repository skill and roles. The bundle owner decides exact models, effort, role names, and fork policy; Planr only carries the resulting opaque route in pick packets. Codex Automations use the same starter line.
106
109
 
107
110
  ### Claude Code
108
111
 
@@ -142,29 +145,9 @@ Any MCP-capable agent uses the same flow over `planr mcp`. Every session starts
142
145
 
143
146
  ## Cost Tiering
144
147
 
145
- A goal run has three roles with different intelligence needs, so they should not all run on the same model tier:
148
+ A goal run has distinct roles with different intelligence needs. Planr represents those decisions as opaque profiles and routes; it does not choose providers, models, effort levels, fallback chains, or host-native role names.
146
149
 
147
- - **Driver** (the `/goal` session): decomposition, dispatch decisions, conflict resolution, final synthesis. Run it on the strongest model you have this is never configured in Planr files, it is simply the model of the main session.
148
- - **Worker**: bounded implementation. `planr pick --json` is a complete handoff packet (one item, scope, evidence format, stop after review request), so the worker runs safely on a cheaper tier.
149
- - **Reviewer**: the truth gate. It inherits the driver's model on purpose — make workers cheap, not the verdict.
150
-
151
- Where each host configures the worker tier (the shipped role files carry these defaults):
152
-
153
- | Host | Driver | Worker | Configured in |
154
- | --- | --- | --- | --- |
155
- | Codex | session default (e.g. `gpt-5.5` at `high`) | `model = "gpt-5.5"`, `model_reasoning_effort = "medium"` | `.codex/agents/planr-worker.toml` |
156
- | Claude Code | session model (e.g. `fable` at `high` via `/model` + `/effort`) | `model: opus`, `effort: medium` | `planr-worker.md` frontmatter |
157
- | Cursor | chat model of the driving session | `model: inherit` by default; pin a cheaper Cursor model id | `.cursor/agents/planr-worker.md` frontmatter |
158
-
159
- The defaults use aliases and generic names so they track model generations; pin a full model id (e.g. `claude-opus-4-8`) only if you need determinism, and use `model: sonnet` as the budget alternative. The role files are user-owned copies — `planr project init` provisions them once and never overwrites local edits — so changing the tier is editing one line.
160
-
161
- To declare the tiering itself — which model handles which work type, with fallbacks for rate limits — use the agent profile registry (`.planr/agents.toml`): the routing recommendation then travels inside every `planr pick --json` packet, so the driver dispatches the right tier without consulting this table mid-run. `planr agents init` writes a working starter registry with these defaults; see [Model Routing](MODEL_ROUTING.md). The role files above still carry the host-native pins (`planr install <client> --force` re-renders them from the registry); the registry is the declared source of truth for the routing decision.
162
-
163
- Traps to verify once per setup — every one of them is silent (the run still works, just at driver prices), which is why the smoke test is worth the two minutes:
164
-
165
- - **Claude Code:** the `CLAUDE_CODE_SUBAGENT_MODEL` environment variable clamps every subagent's `model:` frontmatter and per-invocation model with no signal in the `tool_result` ([anthropics/claude-code#57718](https://github.com/anthropics/claude-code/issues/57718)); since v2.1.196, setting it to `inherit` behaves like unset, so that is the safe pin for "driver decides". Org-managed `availableModels` allowlists are equally quiet: a frontmatter model outside the allowlist falls back to inherit without warning. Make sure the env var is unset (or `inherit`), then dispatch the worker on a trivial item and confirm the subagent's messages in the session log (`~/.claude/projects/<project>/*.jsonl`) carry the worker model, not the driver's.
166
- - **Codex:** the spawn regressions that made children ignore custom agent files ([openai/codex#26868](https://github.com/openai/codex/issues/26868), [#26363](https://github.com/openai/codex/issues/26363)) are fixed in v0.138+, but two traps remain. First, `fork_turns = "all"` (the multi-agent v2 default) intentionally drops the child's `agent_type` and `model` so the fork replays the parent exactly — dispatch that should honor the worker pin needs `fork_turns = "none"` or a partial fork. Second, the role registry is loaded once at session start ([#26408](https://github.com/openai/codex/issues/26408)): after editing or re-rendering `.codex/agents/*.toml`, restart the session, or the child spawns from the stale definitions. Spawn `planr_worker` on a trivial item and confirm the child metadata shows the pinned model and effort with a non-null `agent_path`.
167
- - **Cursor:** `model: <id>` in `.cursor/agents/planr-worker.md` frontmatter can be overridden without an error by team-admin model policies, plan availability (a model your plan lacks resolves to a fallback), and Max-Mode-only models. On legacy request-based plans, subagents are forced onto Composer regardless of the pin. Dispatch the worker once and check which model the subagent transcript reports before trusting the pin.
150
+ An optional routing-policy package can map planning, exploration, implementation, mechanical work, and independent review onto different cost or quality tiers. If a routing bundle generated repository roles, follow its generated instructions and verify effective child metadata because declarations alone are not proof. The official package and its host-specific caveats live under [`planr-routing`](../planr-routing/docs/MODEL_ROUTING_POLICY.md); the Core contract is documented in [Model Routing](MODEL_ROUTING.md).
168
151
 
169
152
  ## Coming From Other Goal Tools
170
153
 
package/docs/INSTALL.md CHANGED
@@ -72,7 +72,7 @@ planr prompt cli --client codex
72
72
  planr prompt http
73
73
  ```
74
74
 
75
- `planr install claude` writes a project `.mcp.json`; `planr install cursor` writes `.cursor/mcp.json` plus the Planr skills under `.cursor/skills/` and prints a one-click user-level MCP deeplink; `planr install codex` writes a project MCP snippet. All install commands also provision the `planr-worker` and `planr-reviewer` subagent role files (`.codex/agents/`, `.claude/agents/`, `.cursor/agents/`) without overwriting existing edits; `planr project init --client <client|all>` does the same at init time. Dry-runs print the exact config and scope notes first.
75
+ `planr install claude` writes a project `.mcp.json` plus static workflow roles. `planr install cursor` writes `.cursor/mcp.json`, static workflow roles, and Planr skills, then prints a user-level MCP deeplink. `planr install codex` writes only the project MCP snippet. Optional model pins and generated host roles come from a repository-local routing bundle. Dry-runs print exact paths first; no command touches global user configuration.
76
76
 
77
77
  Skills-and-agents-only setups (no MCP) use `--no-mcp`: `planr install cursor --no-mcp` writes the subagents and skills but no MCP config — the skills drive the `planr` CLI directly, so nothing is lost except MCP tool access.
78
78
 
@@ -87,7 +87,7 @@ Open `http://127.0.0.1:7526/review` after `planr serve` for the local browser re
87
87
 
88
88
  ## Agent Skills And Plugin
89
89
 
90
- The repository ships a plugin under `plugins/planr` for Codex, Claude Code, and Cursor that bundles the Planr skills (`$planr`, `$planr-loop`, stage and capability skills) and the subagent roles. The plugin carries skills and roles only; the CLI above must be installed separately. See [Skills](SKILLS.md) for plugin install commands and the skill workflow.
90
+ The repository ships a plugin under `plugins/planr` for Codex and Claude Code that bundles Planr workflow skills; Claude Code additionally registers static workflow roles. Cursor receives the same skills and static roles through `planr install cursor`. Model-specific role files are optional routing-bundle artifacts. The CLI above must be installed separately. See [Skills](SKILLS.md).
91
91
 
92
92
  ## From Source
93
93
 
@@ -41,7 +41,8 @@ Required groups:
41
41
  - approval request, approve, deny, and list
42
42
  - artifact add, list, and show
43
43
  - event list and debug bundle preview
44
- - log add and read
44
+ - trace item, log add, and log read (including three-stage route observations)
45
+ - provider-neutral RoutingBundle v1 inspection, repository-safe preview/apply, and durable application evidence
45
46
  - review annotate, ingest, artifact, evidence, and close
46
47
  - item close, context create, and search
47
48
 
@@ -63,7 +64,7 @@ HTTP mirrors the same rule: `GET /v1/reviews/:id/artifact` is read-only; `POST /
63
64
 
64
65
  `planr install <client> --dry-run` prints project-scoped configuration for Codex, Claude Code, and Cursor. Non-dry install writes only repository-local files:
65
66
 
66
- - Codex: `.planr/integrations/codex-mcp.toml` plus `.codex/agents/` roles
67
+ - Codex: `.planr/integrations/codex-mcp.toml`; optional `.codex/agents/` roles are routing-bundle artifacts
67
68
  - Claude Code: `.mcp.json` plus `.claude/agents/` roles
68
69
  - Cursor: `.cursor/mcp.json` plus `.cursor/agents/` roles and `.cursor/skills/` skill copies
69
70
 
@@ -1,196 +1,39 @@
1
- # Model Routing
1
+ # Model routing
2
2
 
3
- Declare which agent and model each kind of work should run on once, in one file and Planr hands the recommendation to whoever picks the work.
4
-
5
- The pattern this makes declarative: strongest model plans and judges, a cheap fast steerable model implements, token-hungry side work (browser verification, codebase analysis) goes to budget profiles. Without a registry that knowledge lives in CLAUDE.md prose, Codex agent TOMLs, and Cursor frontmatter — three dialects that drift. With a registry it travels inside every pick packet.
6
-
7
- Routing is **advisory by design**: Planr never calls model providers and never blocks a pick because a profile is unavailable. Your host (Codex, Claude Code, Cursor, any MCP client) stays the dispatch authority.
8
-
9
- Want the whole flow on a concrete project first? [Worked Example: Routing a Small Web App](EXAMPLE_WEBAPP.md) walks a frontend/backend todo app from pool declaration to the audit trail, with real outputs.
10
-
11
- ## Quick Start
12
-
13
- One command writes a working starter registry — the cost-tiering defaults with a premium driver, a standard implementer, and a budget helper, commented so the tiers explain themselves:
14
-
15
- ```bash
16
- planr agents init # writes .planr/agents.toml; never overwrites without --force
17
- ```
18
-
19
- Or declare `.planr/agents.toml` by hand:
3
+ Planr Core treats routing as optional, advisory repository data. `.planr/agents.toml` declares opaque profiles and routes; Planr resolves them into pick packets but never calls a provider or claims that a requested model actually ran.
20
4
 
21
5
  ```toml
22
- [profiles.fable-driver]
23
- client = "cursor"
24
- model = "fable-5"
6
+ [profiles.worker]
7
+ client = "host-a"
8
+ model = "model-id"
9
+ agent_type = "repository-role"
25
10
  effort = "high"
26
- cost_tier = "premium"
27
- capabilities = ["orchestration", "review", "planning"]
28
- notes = "Planner/architect and judge. Verdicts stay on this tier."
29
-
30
- [profiles.gpt55-coder]
31
- client = "codex"
32
- model = "gpt-5.5"
33
- effort = "xhigh"
34
- cost_tier = "standard"
35
- capabilities = ["code", "steerable"]
36
- notes = "Primary implementer: strong, fast, cheap on subscription."
11
+ skill = "planr-work"
37
12
 
38
13
  [[routes]]
39
14
  match = { work_type = "code" }
40
- profile = "gpt55-coder"
41
- fallbacks = ["fable-driver"]
42
-
43
- [[routes]]
44
- match = { work_type = "review" }
45
- profile = "fable-driver"
46
-
47
- [route_default]
48
- profile = "gpt55-coder"
49
- fallbacks = ["fable-driver"]
50
- ```
51
-
52
- Validate and inspect:
53
-
54
- ```bash
55
- planr agents check # non-zero exit only on parse failure; warnings pass
56
- planr agents list # resolved profiles, routes, and warnings
15
+ profile = "worker"
57
16
  ```
58
17
 
59
- From then on, every pick carries the recommendation:
18
+ Create a neutral scaffold or an explicit registry:
60
19
 
61
20
  ```bash
62
- planr pick --json
63
- ```
64
-
65
- ```json
66
- "routing": {
67
- "profile": "gpt55-coder",
68
- "client": "codex",
69
- "model": "gpt-5.5",
70
- "effort": "xhigh",
71
- "cost_tier": "standard",
72
- "fallbacks": ["fable-driver"],
73
- "matched_selector": "work_type=code"
74
- }
21
+ planr agents init
22
+ planr agents init --profile worker=host-a/model-id@high#standard --route code=worker
23
+ planr agents check
24
+ planr agents list --json
75
25
  ```
76
26
 
77
- A driver session dispatches the right worker from the packet alone and when the primary hits a rate limit, the fallback order is already in hand. No mid-run config edits.
78
-
79
- ## The Registry File
80
-
81
- `.planr/agents.toml` has three parts:
82
-
83
- - `[profiles.<id>]` — a named agent setting. `client` (which host dispatches it: `codex`, `claude-code`, `cursor`, `generic-mcp`) and `model` are required; `effort`, `cost_tier` (`premium` | `standard` | `budget`), `capabilities`, and `notes` are optional. Model ids and aliases pass through verbatim — Planr does not validate them against provider catalogs, so new models need no Planr release.
84
- - `[[routes]]` — `match` selects work (`work_type = "code"` or `plan = "pln-1234abcd"`), `profile` names the primary, `fallbacks` the ordered alternatives.
85
- - `[route_default]` — catches everything no route matched.
86
-
87
- Resolution precedence per item: **per-item override > `work_type` route > `plan` route > default**. Within a level, the first declared route wins. If a chain's primary profile id is unknown, the first known fallback is promoted; a chain with no known profiles falls through to the next precedence level, so a typo never swallows lower routes. `matched_selector` in the output tells you which rule fired (`override`, `work_type=<v>`, `plan=<v>`, or `default`).
27
+ Resolution order is per-item override, work type, plan, then default route. Unknown profiles fail open to the next applicable route. Host names, model ids, role selectors, effort values, and fallback behavior are opaque to Core.
88
28
 
89
- ## Per-Item Overrides
29
+ Workers may report observed routing with logs and route-audit evidence. Requested-only values never become effective proof; missing effective evidence remains explicitly unavailable.
90
30
 
91
- When one item needs a different setting than its policy route a gnarly refactor that deserves the premium tier, a bulk doc pass that can run on budget — pin it:
31
+ Host-specific model policies and generated repository roles are optional. The `planr-routing` workspace package compiles them into RoutingBundle v1, and Core safely previews and applies that bundle:
92
32
 
93
33
  ```bash
94
- planr item route <item-id> # resolved route + source: override or policy
95
- planr item route <item-id> --set fable-driver # pin; validates the profile id, emits route_overridden
96
- planr item route <item-id> --clear # unpin; policy applies again, emits route_override_cleared
34
+ planr-routing compile balanced --host codex-openai --output routing-bundle.json
35
+ planr routing bundle preview routing-bundle.json
36
+ planr routing bundle apply routing-bundle.json
97
37
  ```
98
38
 
99
- The pin beats every policy route and shows up in the pick packet with `"matched_selector": "override"`. Overrides are repair-friendly: `--set` rejects a profile id the registry does not declare (when the registry is missing or malformed it warns and stores anyway, so offline edits stay possible), and a pin whose profile is later deleted from the registry is never an error policy routing takes over and `item route` prints a repair hint. Both mutations are recorded as graph events, so `planr event list --item <id>` shows who re-routed what, when.
100
-
101
- Tier the roles, not just the models: workers run safely on cheaper tiers because the pick packet bounds their scope, while review verdicts should stay on the strongest tier — `agents check` warns when review work routes to a `budget` profile. Background: [Cost Tiering](GOALS.md#cost-tiering).
102
-
103
- ## Host-Native Rendering
104
-
105
- Routes only matter if the host actually dispatches the declared model, so `planr install codex|claude|cursor` closes the gap: when a registry is present, the provisioned subagent role files are rendered with pins taken from it instead of the shipped static defaults. The `work_type=code` route pins the worker role, the `work_type=review` route pins the reviewer role, and each render uses the host's exact vocabulary — Codex TOML gets `model` and `model_reasoning_effort` (with `developer_instructions` always present, since Codex silently ignores a role file without it), Claude frontmatter gets `model:` and `effort:`, Cursor frontmatter gets `model:` only.
106
-
107
- Two safety rules keep this predictable:
108
-
109
- - **Client matching**: a role file only pins profiles whose `client` matches the install target, scanning the route's fallback chain for the first match. A review route pointing at a Cursor profile never writes a Cursor model id into a Codex TOML — that role keeps its static default instead.
110
- - **Provision-once**: existing files are never overwritten. After editing the registry, re-render explicitly with `planr install <client> --force`. Rendered files start with a `# generated from .planr/agents.toml` header so you (and future audit tooling) can tell them from hand-maintained ones.
111
-
112
- Without a registry, installs write the static role files byte-identically to previous releases.
113
-
114
- ## Prompt Routing
115
-
116
- `planr prompt routing [--client codex|claude|cursor|all]` prints a paste-ready block for the driver session: the prioritization table (every route, profile, and fallback in precedence order), per-host dispatch guidance including the traps that silently defeat pins (Codex requires `fork_turns: "none"` and a session restart after re-rendering; the `CLAUDE_CODE_SUBAGENT_MODEL` env var preempts Claude frontmatter; Cursor plan mode, admin policy, and Max Mode override silently), and process-dispatch snippets (`codex exec`, `pi`, `opencode run`) for hosts without role files, pre-filled from the `work_type=code` route. `--json` carries the same content structured.
117
-
118
- ## Run Audit
119
-
120
- Every host has a silent override path — the `CLAUDE_CODE_SUBAGENT_MODEL` env var, Cursor plan/admin/Max-Mode policy, Codex full-history forks, org allowlists — so a pin alone is not proof. The audit loop closes this: workers report the profile they actually ran on via `planr log add`/`planr done --profile <id>` (or the `PLANR_PROFILE` env var, which rendered role files can export), the profile lands on the recorded run, and when it differs from the item's declared route Planr emits an advisory `route_mismatch_observed` event with the declared and actual ids.
121
-
122
- - `planr trace item <id>` shows the declared route next to every run's actual client/profile with a `mismatch` marker.
123
- - Runs also record the host they observably executed under (`observed_client`, detected from environment variables the hosts set themselves — no flags); a run whose host differs from the declared route's client emits an advisory `client_mismatch_observed` event, which catches exactly the deviation profile self-report cannot: a different host standing in for the declared client, even when the model matched.
124
- - `planr doctor` reports the registry state (absent, degraded with parse context, loaded with counts and warnings) and flags rendered role files that drifted from the current registry (`planr install <client> --force` re-renders).
125
- - `planr export`/`import` carry the registry with the package, preview-first; an existing registry at the destination is never silently overwritten.
126
-
127
- Everything here is advisory (ADR-001): mismatches never fail logging, reviews, or closes. No profile reported, no run recorded, or no registry means no comparison and no event.
128
-
129
- One legitimate mismatch source to know: a driver adding a live-verification log to a routed item runs on the driver profile by design, which emits a `route_mismatch_observed` event. The payload carries `log_kind`, so audit consumers can discount `verification` entries and alarm only on `completion` mismatches.
130
-
131
- For single-host pools (e.g. all-Cursor), declare the host's *exact* model slugs (`claude-opus-4-8-thinking-high`, not `opus`): dispatch APIs resolve slugs, not aliases, and a driver forced to map `fable-5` onto the nearest slug at dispatch time is a silent translation the audit cannot see.
132
-
133
- ## Failure Behavior
134
-
135
- - **No registry file**: nothing changes. Pick packets simply have no `routing` key.
136
- - **Malformed registry**: `planr agents check` fails with the parser's line context; everything else (`pick`, `map`, `install`) keeps working with routing omitted — installs fall back to the static role files.
137
- - **Warnings** (unknown profile references, empty or duplicate selectors, budget-tier review routes, secret-like values) never block anything; `agents check` lists them and still exits zero.
138
- - Never put credentials in the registry — it holds configuration strings only, and secret-like values are flagged.
139
-
140
- ## Use-Case Pools
141
-
142
- Work types are free-form, and that makes them the use-case dimension: beyond the built-in vocabulary (`code`, `fix`, `review`, `docs`, ...), any string you pass to `--work-type` routes. Combined with per-profile skill pairing, the registry becomes a small agent pool — each use case names who runs it, on what model, with which skill:
143
-
144
- ```toml
145
- [profiles.designer]
146
- client = "claude-code"
147
- model = "opus"
148
- effort = "high"
149
- cost_tier = "premium"
150
- skill = "frontend-design" # dispatch this profile *with* this skill
151
-
152
- [profiles.backender]
153
- client = "codex"
154
- model = "gpt-5.5"
155
- effort = "xhigh"
156
- cost_tier = "standard"
157
- skill = "planr-work"
158
-
159
- [[routes]]
160
- match = { work_type = "frontend" }
161
- profile = "designer"
162
- fallbacks = ["driver"]
163
-
164
- [[routes]]
165
- match = { work_type = "design" }
166
- profile = "designer"
167
-
168
- [[routes]]
169
- match = { work_type = "backend" }
170
- profile = "backender"
171
- fallbacks = ["driver"]
172
- ```
173
-
174
- Create items with the use-case work type (`planr item create ... --work-type frontend`) — or retag existing ones with `planr item update <id> --work-type frontend`, which is how planning agents tag `map build` output against the declared routes (the planning skills read `agents list` and do this without user involvement) — and the pick packet carries the full pairing — `"profile": "designer"`, `"model": "opus"`, `"skill": "frontend-design"` — so the driver dispatches profile and skill together (`Use $frontend-design on item <id>` on the profile's client/model). Workers pull their slice of the pool with `planr pick --work-type frontend`. `skill` is passthrough vocabulary like model ids: Planr never validates it against installed skills, and profiles without one omit the key entirely. A profile that needs different skills for different use cases is simply two profiles.
175
-
176
- Declare the `client` you will actually dispatch on. A loop running inside one host dispatches that host's subagents — an in-Cursor driver that dispatches Cursor subagents with per-dispatch models is running `client = "cursor"` profiles in practice, even when the model matches. A `client = "codex"` profile is only honest when the driver really spawns a Codex process (`codex exec ...`). This matters for the audit: workers report the *profile id*, so a profile whose declared client differs from the real dispatch host passes the mismatch check on the model alone — the client deviation stays invisible.
177
-
178
- When do you actually need more than one client? Hosts with a full model catalog (Cursor) can serve an entire pool natively — an all-`cursor` registry with different models per profile is the normal case there. Cross-client profiles exist for two real situations: vendor-locked hosts (Claude Code dispatches only Anthropic models, Codex CLI only OpenAI models — a Claude-Code driver that wants a GPT implementer must process-dispatch via `codex exec`), and subscription economics (the same model can bill differently per host, so routing backend work through a flat-rate CLI subscription instead of the driver host's quota is a legitimate cost decision).
179
-
180
- ## Host Matrix
181
-
182
- Where each host reads its model configuration from, and what silently defeats a pin there (state of July 2026):
183
-
184
- | Host | Native mechanism | Rendered by `planr install`? | Silent overrides / traps |
185
- | --- | --- | --- | --- |
186
- | Cursor | `.cursor/agents/*.md` frontmatter `model: <id>` (default `inherit`) | yes (`cursor`) | Team-admin model policy, plan availability, and Max-Mode-only models override without error; legacy request-based plans force Composer for subagents; subagent transcripts record no model field, so the actual model cannot be verified from artifacts after the fact — the dispatch parameters in the driver session are the only record |
187
- | Claude Code | `planr-worker.md`/`planr-reviewer.md` frontmatter `model:` + `effort:` | yes (`claude`) | `CLAUDE_CODE_SUBAGENT_MODEL` clamps frontmatter and per-invocation models with no signal ([#57718](https://github.com/anthropics/claude-code/issues/57718)); since v2.1.196 `inherit` behaves as unset; org `availableModels` allowlists fall back silently |
188
- | Codex CLI | `.codex/agents/*.toml` with `model` + `model_reasoning_effort` | yes (`codex`) | `fork_turns = "all"` intentionally drops the child's `agent_type`/`model` — use `fork_turns = "none"` or a partial fork; the role registry loads at session start ([#26408](https://github.com/openai/codex/issues/26408)), so re-renders need a restart |
189
- | opencode | `opencode.json` `agent.<name>.model = "provider/model-id"` or `.opencode/agents/*.md` frontmatter | no — use the `planr prompt routing` process snippet | Subagent inherits the primary model when unset; malformed `provider/model-id` strings (quoting, trailing newline) raise `ProviderModelNotFoundError` ([#5623](https://github.com/sst/opencode/issues/5623)) |
190
- | Pi | none by design — process-level dispatch (`pi --provider --model --thinking`) or the `pi-subagents` extension (`.pi/agents/*.md`) | no — use the `planr prompt routing` process snippet | Extension model-scope enforcement against `enabledModels` is opt-in; without it, pins are best-effort |
191
-
192
- For the hosts without rendered role files, `planr prompt routing` prints ready process-dispatch snippets pre-filled from the registry. Whatever the host does, the [run audit](#run-audit) catches silent overrides after the fact.
193
-
194
- ## Command Summary
195
-
196
- The registry surface end to end: `planr agents init [--force]` scaffolds, `planr agents list|check` inspect and validate, `planr pick --json` carries the `routing` block, `planr item route [--set|--clear]` pins per item, the MCP tools (`planr_agents_list`, `planr_item_route`, `planr_item_route_set`, `planr_item_route_clear`) return identical JSON shapes, `planr install <client> [--force]` renders host role files from the registry, `planr prompt routing` prints the driver dispatch block, `planr log add`/`done --profile` (or `PLANR_PROFILE`) feed the run audit, `planr trace item` and `planr doctor` surface mismatches and drift, and `planr export`/`import` carry the registry preview-first.
39
+ Bundle application is restricted to the repository. Planr never edits user configuration such as `~/.codex/config.toml`. See [Routing Bundles](ROUTING_BUNDLES.md).
@@ -0,0 +1,32 @@
1
+ # Routing bundles
2
+
3
+ Planr Core is provider-neutral. It parses `.planr/agents.toml`, resolves routes into pick packets, records declared-versus-observed evidence, and safely previews or applies a strict RoutingBundle v1:
4
+
5
+ ```bash
6
+ planr routing bundle inspect routing-bundle.json
7
+ planr routing bundle preview routing-bundle.json
8
+ planr routing bundle apply routing-bundle.json
9
+ ```
10
+
11
+ Core accepts only allowlisted repository-local targets, verifies payload hashes, rejects absolute paths, traversal, symlinks, parent/child target collisions, conflicts, unsupported versions, and invalid payloads, and applies the validated set atomically. It never writes user configuration or files outside the repository.
12
+
13
+ A signed bundle is accepted only with an independent trust anchor supplied to every inspect, preview, or apply call:
14
+
15
+ ```bash
16
+ planr routing bundle inspect signed-bundle.json \
17
+ --trusted-signer planr-maintainers \
18
+ --trusted-public-key-file /absolute/path/to/maintainer.pub
19
+ ```
20
+
21
+ The bundle contains the signer id and signature, not a self-trusted public key. Both trust flags are required together; unsigned bundles require neither. An unsigned bundle also cannot label its evidence `verified` or `recommended`.
22
+
23
+ The `planr-routing` workspace package owns all volatile opinions: named policies, exact model ids, host bindings, generated role and skill files, capability probes, evaluation scenarios, signing, registry data, and the website catalog. A normal flow is:
24
+
25
+ ```bash
26
+ planr-routing policy list
27
+ planr-routing compile balanced --host codex-openai --output routing-bundle.json
28
+ planr routing bundle preview routing-bundle.json
29
+ planr routing bundle apply routing-bundle.json
30
+ ```
31
+
32
+ The package emits the same provider-neutral bundle contract for Codex, Claude Code, Cursor, and mixed-host configurations. Offline evaluation remains experimental; a recommendation requires complete authenticated live-host evidence. Missing authentication or missing effective model, effort, role, or context-fork evidence cannot pass.
package/docs/SKILLS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Planr ships agent-facing skill templates under `plugins/planr/skills/`.
4
4
 
5
- The repository ships an installable plugin under `plugins/planr` for Codex, Claude Code, and Cursor, so the skills can be installed as one package instead of copied by hand. Marketplace manifests at the repo root (`.agents/plugins/marketplace.json`, `.claude-plugin/marketplace.json`) point at that subdirectory — Codex silently ignores marketplaces whose plugin source is the repo root itself. The plugin only carries skills and agent roles; the `planr` CLI must be installed separately (`brew install instructa/tap/planr`).
5
+ The repository ships an installable plugin under `plugins/planr` for Codex and Claude Code, while Cursor receives the same skills through `planr install cursor`. Marketplace manifests at the repo root (`.agents/plugins/marketplace.json`, `.claude-plugin/marketplace.json`) point at that subdirectory — Codex silently ignores marketplaces whose plugin source is the repo root itself. The shared package carries skills and Claude's independent workflow roles; optional model-specific host roles come only from a repository-local routing bundle, never from Planr Core or static fallbacks. The `planr` CLI must be installed separately (`brew install instructa/tap/planr`).
6
6
 
7
7
  ## Install As Plugin (preferred)
8
8
 
@@ -165,10 +165,12 @@ The CLI provisions the role files automatically — no manual copying:
165
165
 
166
166
  ```bash
167
167
  planr project init "My Product" --client all # writes .codex/agents/*.toml, .claude/agents/*.md, and .cursor/agents/*.md
168
- planr install codex # provisions roles for an existing project (same for claude and cursor)
168
+ planr agents init # generates canonical native Codex roles in an existing project
169
+ planr install claude # provisions Claude's independent roles
170
+ planr install cursor # provisions Cursor's independent roles and skills
169
171
  ```
170
172
 
171
- Codex needs these project-scoped files because its plugin system carries skills only; the Claude Code and Cursor plugins register the same roles automatically, so the provisioned `.claude/agents/` and `.cursor/agents/` copies only matter for standalone (non-plugin) installs. Existing role files are never overwritten.
173
+ Optional project-scoped model-routing files are generated by `planr-routing` bundles. Core workflow skills remain host-neutral, and bundle application never overwrites conflicts or writes user configuration.
172
174
 
173
175
  Dispatches stay one line: `Use $planr-work on item <id>` and `Use $planr-review on item <id>`. The map and logs are the loop memory, so any iteration can resume from zero context.
174
176
 
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "tools": [
3
3
  "planr_project_show",
4
+ "planr_trace_item",
4
5
  "planr_map_show",
5
6
  "planr_map_status",
6
7
  "planr_map_preview",
@@ -19,6 +20,9 @@
19
20
  "planr_item_amend",
20
21
  "planr_item_replan",
21
22
  "planr_agents_list",
23
+ "planr_policy_show",
24
+ "planr_policy_check",
25
+ "planr_policy_admit",
22
26
  "planr_item_route",
23
27
  "planr_item_route_set",
24
28
  "planr_item_route_clear",
@@ -99,7 +103,11 @@
99
103
  "planr recover sweep",
100
104
  "planr agents list",
101
105
  "planr agents check",
106
+ "planr routing bundle inspect",
107
+ "planr routing bundle preview",
108
+ "planr routing bundle apply",
102
109
  "planr item route",
110
+ "planr trace item",
103
111
  "planr serve --port"
104
112
  ]
105
113
  }
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planr",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Local-first planning and execution coordination for coding agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,7 +22,8 @@
22
22
  "scripts": {
23
23
  "build:native": "cargo build --release",
24
24
  "test": "cargo test",
25
- "pack:check": "npm pack --dry-run"
25
+ "pack:check": "npm pack --dry-run",
26
+ "routing": "cargo run --manifest-path planr-routing/Cargo.toml --"
26
27
  },
27
28
  "engines": {
28
29
  "node": ">=18"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "planr",
3
3
  "description": "Skill-driven planning and execution loop for coding agents: one planr entry point, an autonomous planr-loop, and evidence-backed task graph skills powered by the planr CLI.",
4
- "version": "1.3.0",
4
+ "version": "1.5.0",
5
5
  "author": {
6
6
  "name": "instructa"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planr",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Skill-driven planning and execution loop for coding agents: one $planr entry point, an autonomous $planr-loop, and evidence-backed task graph skills powered by the planr CLI.",
5
5
  "author": {
6
6
  "name": "instructa",
@@ -67,7 +67,7 @@ A worker subagent may take several items sequentially instead of being respawned
67
67
 
68
68
  Host wiring:
69
69
 
70
- - Codex: project agents in `.codex/agents/*.toml` preload the skill via `[[skills.config]]` (TOML templates in `agents/` next to this skill). Spawn explicitly: "spawn the planr_worker agent for item X". Keep `[agents] max_depth = 1`.
70
+ - When a repository routing bundle supplies host roles, follow its generated routing skill exactly. Otherwise treat the profile in the pick packet as advisory and use the host's current dispatch contract. Never infer effective model or effort from a declaration alone.
71
71
  - Claude Code: subagents preload via the `skills:` frontmatter field. The Planr plugin registers `planr-worker` and `planr-reviewer` automatically from its `agents/` directory; standalone installs copy them to `.claude/agents/`. The reviewer subagent is read-only except for `planr review` commands.
72
72
  - Cursor: project subagents in `.cursor/agents/*.md` (Markdown templates in `agents/` next to this skill; `planr install cursor` provisions them). Dispatch explicitly: `/planr-worker implement item X`, `/planr-reviewer review item X`. Parallel dispatches are safe — the map's pick lease keeps one owner per item.
73
73
  - Single-agent hosts: run worker and checker as separate sequential dispatches with a fresh read of map state in between; never carry the worker's self-assessment into the review step. The mode is recorded automatically: `review close` derives `review_mode` (`single_agent`/`independent`) from worker identity.
@@ -1,23 +0,0 @@
1
- # Copy to .codex/agents/planr-reviewer.toml (project) or ~/.codex/agents/ (user).
2
- # Adjust the skill path to where the Planr skills are installed.
3
- name = "planr_reviewer"
4
- description = "Independent findings-first reviewer for one Planr item. Audits evidence and closes the review with a verdict."
5
- sandbox_mode = "workspace-write"
6
-
7
- # Deliberately no model override: the reviewer is the truth gate and inherits
8
- # the driver session's model. Make workers cheap, not the verdict.
9
-
10
- developer_instructions = """
11
- Use the planr-review skill exactly as written for the single item id you are given.
12
- You did not write this code; audit it like an owner. Inspect the actual diff and rerun the
13
- logged verification commands instead of trusting the worker's summary.
14
- Close the review with `planr review close <review-id> --verdict ... --reviewer <your-id>` and
15
- always pass `--reviewer` explicitly (e.g. `checker-1`): shell exports do not survive between
16
- tool calls, and a review closed under the default identity corrupts the independence audit.
17
- Findings must be specific and actionable. Do not edit implementation files; your only writes
18
- are planr review commands.
19
- """
20
-
21
- [[skills.config]]
22
- path = "~/.codex/skills/planr-review/SKILL.md"
23
- enabled = true
@@ -1,21 +0,0 @@
1
- # Copy to .codex/agents/planr-worker.toml (project) or ~/.codex/agents/ (user).
2
- # Adjust the skill path to where the Planr skills are installed.
3
- name = "planr_worker"
4
- description = "Implements exactly one picked Planr map item to evidence-backed completion, then requests review and stops."
5
-
6
- # Cost tiering: the pick packet bounds the worker's scope, so it can run on a
7
- # lower effort tier than the driver session. Verify the pin took effect after
8
- # the first spawn (some Codex versions ignore custom agent files on spawn —
9
- # openai/codex#26868); see docs/GOALS.md "Cost Tiering" for the smoke test.
10
- model = "gpt-5.5"
11
- model_reasoning_effort = "medium"
12
-
13
- developer_instructions = """
14
- Use the planr-work skill exactly as written for the single item id you are given.
15
- Implement only that item. Log changed files and the real verification commands you ran.
16
- Request review with `planr review request <item-id>` and stop. Never close reviews or items yourself.
17
- """
18
-
19
- [[skills.config]]
20
- path = "~/.codex/skills/planr-work/SKILL.md"
21
- enabled = true