| # Routing bundles | ||
| 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: | ||
| ```bash | ||
| planr routing bundle inspect routing-bundle.json | ||
| planr routing bundle preview routing-bundle.json | ||
| planr routing bundle apply routing-bundle.json | ||
| ``` | ||
| 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. | ||
| A signed bundle is accepted only with an independent trust anchor supplied to every inspect, preview, or apply call: | ||
| ```bash | ||
| planr routing bundle inspect signed-bundle.json \ | ||
| --trusted-signer planr-maintainers \ | ||
| --trusted-public-key-file /absolute/path/to/maintainer.pub | ||
| ``` | ||
| 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`. | ||
| 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: | ||
| ```bash | ||
| planr-routing policy list | ||
| planr-routing compile balanced --host codex-openai --output routing-bundle.json | ||
| planr routing bundle preview routing-bundle.json | ||
| planr routing bundle apply routing-bundle.json | ||
| ``` | ||
| 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. |
@@ -9,3 +9,3 @@ # Planr Architecture | ||
| - `tests/e2e.rs`: real CLI, MCP, HTTP, import, review-gate, run-log, and concurrent-pick tests. | ||
| - `plugins/planr/`: the installable plugin payload — all nine skills, the worker and reviewer subagent roles, and the per-host plugin manifests. | ||
| - `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. | ||
| - `.agents/plugins/marketplace.json`, `.claude-plugin/marketplace.json`: marketplace manifests pointing Codex and Claude Code at `plugins/planr`. | ||
@@ -47,8 +47,8 @@ - `docs/`: user and contributor guides; `docs/planr-spec/` is the production specification package for Planr V1. | ||
| - `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. | ||
| - `src/preset.rs`: pure preset-composition core. Owns versioned host-binding types, abstract-role compatibility, model/effort/fork capability validation, deterministic registry projection, verification age, permission additions, and provenance-lock vocabulary; it performs no writes. | ||
| - `src/app/presets.rs`: shared CLI/MCP preset application service and repository-artifact writer. Owns preview/conflict reporting, canonical-root and allowlist enforcement, symlink/traversal rejection, create-only apply, rollback on write failure, and `policy_applied`/rejection events. | ||
| - `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. | ||
| - `planr-routing/`: independently buildable workspace package that owns model policies, host bindings, generated host artifacts, probes, evaluation, signing, registry data, and catalog publication. | ||
| - `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. | ||
| - `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. | ||
| - `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. | ||
| - `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. | ||
| - `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. | ||
| - `src/util.rs`: small CLI-boundary utilities. Owns ids, timestamps, path helpers, output formatting, and safe file writes. | ||
@@ -55,0 +55,0 @@ |
@@ -70,7 +70,5 @@ # CLI Reference | ||
| planr agents check | ||
| planr agents preset list [--json] | ||
| planr agents preset apply <policy-path-or-id> --binding <binding-path-or-id> [--preview|--confirm] | ||
| planr agents preset registry verify <manifest> --entry <id> --content-root <dir> [--trust-store <path>] [--at-unix <timestamp>] [--host <host>] | ||
| planr agents preset registry import <manifest> --entry <id> --content-root <dir> [--trust-store <path>] [--at-unix <timestamp>] [--host <host>] [--preview|--confirm] | ||
| planr agents preset registry list [--at-unix <timestamp>] | ||
| planr routing bundle inspect <bundle.json> | ||
| planr routing bundle preview <bundle.json> | ||
| planr routing bundle apply <bundle.json> | ||
| planr doctor [--client codex|claude|cursor|all] | ||
@@ -147,12 +145,6 @@ planr install codex|claude|cursor [--dry-run] [--no-mcp] [--force] [--no-hooks] | ||
| `agents preset list` enumerates the embedded catalog: `balanced`, `low-usage`, `max-quality`, and `read-only-audit` policies; `codex-openai`, `cursor-openai`, `cursor-fable-grok`, `claude-native`, and `mixed-host` bindings; source checksums; and every declared safe pair. MCP `planr_presets_list` returns the identical catalog. | ||
| `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). | ||
| `agents preset apply <policy-path-or-id> --binding <binding-path-or-id>` composes a provider-neutral Usage Policy v1 file with a strict, versioned host binding. Embedded id pairs report `pack.status = safe`; explicit file inputs remain allowed custom compositions and receive a clear non-safe warning even if they reuse built-in ids. Preview is the default and reports compatibility, per-role permission additions, verification age, provenance, content hashes, every normalized repository-relative target, create/unchanged/conflict state, and a redacted old/proposed configuration projection. Policy limits/transitions and generated registry routes, concrete models/efforts, and fallbacks are therefore auditable before confirmation without returning secret-like values. `--confirm` writes exactly the conflict-free preview to `.planr/policy.toml`, `.planr/agents.toml`, `.planr/preset.lock.toml`, and allowlisted repository-local host artifacts; it never overwrites different existing files. Absolute paths, traversal, symlink crossings, user/home/global configuration, and repository `.codex/config.toml` fail before the first write. Secret-like binding metadata is rejected with field-only diagnostics before dispatch/warning output or mutation. Bindings own exact client/model/effort and fork capabilities: Codex cross-tier dispatch is keyed by the binding host identity, defaults to `none`, rejects `all`, and accepts a positive partial fork only with declared limits and non-blank capability evidence. Host-specific bindings require every profile client to match the host; explicit cross-client mappings use `host = "mixed-host"`. MCP `planr_preset_apply` calls the same application service and returns the identical shape (`confirm` defaults to false). Binding schema and repository-boundary details: [Preset Composition](PRESET_COMPOSITION.md). | ||
| `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). | ||
| `agents preset evaluate [--report-dir <dir>] [--at-unix <timestamp>] [--host <host>] [--live-host-command <absolute-executable> [--live-host-arg <arg>]... [--trusted-telemetry-signer <registry-id> --trusted-telemetry-collector <absolute-executable>]]` defaults to the embedded offline simulation: no process/network execution, estimated projections, unavailable effective routes, and no recommendation. Explicit live mode creates a Planr-controlled challenge/workspace for every versioned task. The adapter must read the challenge and write a bounded strict-schema artifact whose independently computed hash, candidate/task/input/artifact binding, challenge binding, and task oracle Planr validates. Live `evidence_complete` requires every required task execution and oracle pass; missing or invalid artifacts produce `unverified` candidates and make report-level `reproducible_evidence` false. Policy tool/write capabilities remain authoritative. Without telemetry, host route and tool/token/credit claims remain estimated and recommendation-ineligible. The all-or-none telemetry flags select a signer and collector independently pinned in `.planr/trusted-telemetry.toml`. Planr creates a fresh run UUID and task challenge, invokes the hash-pinned collector only after independently reading the artifact, verifies the registry-pinned Ed25519 signature and exact run/suite/time/task/input/artifact/challenge/host bindings, then promotes only receipt-backed effective route and usage dimensions to `telemetry_receipt`/`trusted`. Caller-supplied keys, run ids, and precomputed bundles are not accepted; missing, replayed, or tampered post-run receipts fail closed. Valid complete telemetry makes `recommended` reachable on CLI and MCP. Constant responders, public-label request mappers, and read-only candidates on write fixtures cannot recommend. Full protocol: [Preset Evaluation](PRESET_EVALUATION.md). | ||
| `agents preset registry verify/import/list` implements the optional declarative registry boundary. Verification checks version compatibility, host compatibility, lifecycle/revocation, exact byte lengths and SHA-256 digests, canonical policy/binding semantics and safe artifacts, and optional Ed25519 signatures against separately pinned maintainer keys. `recommended` additionally requires a current canonical evaluation bound to the shipped policy/binding bytes, independently derived metrics and thresholds, task/oracle/result-hash completeness, and trusted route/metering evidence; a trusted signature alone cannot promote it. Import previews by default and, on `--confirm`, atomically caches only manifest-declared files plus the original manifest below `.planr/registry/cache/`. Offline listing treats the receipt as inventory only and re-verifies the manifest hash, signature, manifest-owned checksums, and freshness, so coordinated content/receipt tampering fails closed and a registry outage cannot affect active projects or already verified content. See [Preset Registry](PRESET_REGISTRY.md). | ||
| `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). | ||
| `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. | ||
@@ -168,4 +160,4 @@ | ||
| `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. | ||
| `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. | ||
| `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. |
+6
-6
@@ -25,11 +25,11 @@ # Codex Integration | ||
| 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: | ||
| Optionally compile and apply a repository-local Codex routing bundle before starting the driver: | ||
| ```toml | ||
| # .codex/agents/planr-worker.toml | ||
| model = "gpt-5.5" | ||
| model_reasoning_effort = "medium" | ||
| ```bash | ||
| planr-routing compile balanced --host codex-openai --output routing-bundle.json | ||
| planr routing bundle preview routing-bundle.json | ||
| planr routing bundle apply routing-bundle.json | ||
| ``` | ||
| 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). | ||
| 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). | ||
@@ -36,0 +36,0 @@ ## MCP |
+62
-96
| # Worked Example: Routing a Small Web App | ||
| 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. | ||
| 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. | ||
| The pool we want: | ||
| ## 1. Create the project and registry | ||
| | Use case | Who runs it | Paired skill | | ||
| | --- | --- | --- | | ||
| | planning + review | Fable on Cursor (the driver session) | — | | ||
| | frontend, design | Opus on Claude Code | `frontend-design` | | ||
| | backend | GPT-5.5 on Codex | `planr-work` | | ||
| ## Step 1: Project and pool — two commands, once | ||
| ```bash | ||
| planr project init "Todo Webapp" | ||
| planr agents init | ||
| ``` | ||
| planr agents init \ | ||
| --profile driver=cursor/fable-5@high#premium \ | ||
| --profile frontender=claude-code/opus@high#premium \ | ||
| --profile backender=codex/gpt-5.5@xhigh#standard \ | ||
| --skill frontender=frontend-design \ | ||
| --skill backender=planr-work \ | ||
| --route frontend=frontender,driver \ | ||
| --route backend=backender,driver \ | ||
| --route review=driver \ | ||
| --default-route backender,driver | ||
| Edit the repository-local `.planr/agents.toml` with profiles owned by your team or routing package: | ||
| planr agents check | ||
| ```toml | ||
| [profiles.frontend-worker] | ||
| client = "your-host" | ||
| model = "your-fast-model" | ||
| effort = "medium" | ||
| cost_tier = "budget" | ||
| skill = "planr-work" | ||
| [profiles.backend-worker] | ||
| client = "your-host" | ||
| model = "your-engineering-model" | ||
| effort = "high" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| [profiles.independent-reviewer] | ||
| client = "your-host" | ||
| model = "your-review-model" | ||
| effort = "high" | ||
| cost_tier = "premium" | ||
| skill = "planr-review" | ||
| [[routes]] | ||
| match = { work_type = "frontend" } | ||
| profile = "frontend-worker" | ||
| [[routes]] | ||
| match = { work_type = "backend" } | ||
| profile = "backend-worker" | ||
| [[routes]] | ||
| match = { work_type = "review" } | ||
| profile = "independent-reviewer" | ||
| ``` | ||
| ```text | ||
| wrote .planr/agents.toml (generated from the flag specs) | ||
| agent registry check passed | ||
| Validate without contacting a provider: | ||
| ```bash | ||
| planr agents check | ||
| planr agents list | ||
| ``` | ||
| 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. | ||
| Alternatively, an external policy package can compile a strict RoutingBundle v1. Preview and apply it through the generic Core boundary: | ||
| 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. | ||
| ```bash | ||
| planr routing bundle inspect routing-bundle.json | ||
| planr routing bundle preview routing-bundle.json | ||
| planr routing bundle apply routing-bundle.json | ||
| ``` | ||
| ## Step 2: Items carry the use case as their work type | ||
| ## 2. Tag items by use case | ||
| Work types are free-form, so the use case is just `--work-type`: | ||
| ```bash | ||
| planr item create "API: task CRUD endpoints" \ | ||
| --description "Express routes GET/POST/PATCH/DELETE /api/tasks with sqlite store" \ | ||
| --description "GET/POST/PATCH/DELETE /api/tasks with sqlite store" \ | ||
| --work-type backend | ||
| planr item create "UI: task list and add form" \ | ||
| --description "React list view + optimistic add form against /api/tasks" \ | ||
| --description "List view and optimistic add form" \ | ||
| --work-type frontend | ||
| planr item create "UI: design pass on empty and error states" \ | ||
| --description "Empty state, loading skeleton, error toast" \ | ||
| --work-type frontend | ||
| ``` | ||
| ## Step 3: The pick decides who works | ||
| ## 3. Pick and dispatch | ||
| Same board, two different answers. The backend pull: | ||
| ```bash | ||
@@ -67,73 +82,24 @@ planr pick --work-type backend --json | ||
| ```json | ||
| "routing": { | ||
| "profile": "backender", | ||
| "client": "codex", | ||
| "model": "gpt-5.5", | ||
| "effort": "xhigh", | ||
| "cost_tier": "standard", | ||
| "skill": "planr-work", | ||
| "fallbacks": ["driver"], | ||
| "matched_selector": "work_type=backend" | ||
| } | ||
| ``` | ||
| 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. | ||
| And the frontend pull: | ||
| ## 4. Close with evidence | ||
| ```bash | ||
| planr pick --work-type frontend --json | ||
| ``` | ||
| ```json | ||
| "routing": { | ||
| "profile": "frontender", | ||
| "client": "claude-code", | ||
| "model": "opus", | ||
| "skill": "frontend-design", | ||
| "fallbacks": ["driver"], | ||
| "matched_selector": "work_type=frontend" | ||
| } | ||
| ``` | ||
| 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. | ||
| ## Step 4: Workers close with evidence and their profile | ||
| ```bash | ||
| planr done <item-id> \ | ||
| --summary "CRUD endpoints implemented; 8 supertest cases green" \ | ||
| --summary "CRUD endpoints implemented; 8 tests green" \ | ||
| --files server/routes/tasks.ts \ | ||
| --tests "vitest --run: 8 passed" \ | ||
| --profile backender | ||
| --profile backend-worker | ||
| ``` | ||
| `--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. | ||
| `--profile` records the run's claimed profile. For stronger evidence, attach a route observation that keeps requested, host-resolved, and effective values separate. | ||
| ## Step 5: The trace proves the plan was followed | ||
| ## 5. Trace declared versus actual | ||
| 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: | ||
| ```bash | ||
| planr trace item <backend-item> | ||
| planr trace item <frontend-item> | ||
| planr trace item <item-id> | ||
| ``` | ||
| ```text | ||
| routing declared: backender (work_type=backend) | ||
| run run-5552fad6 profile backender | ||
| A mismatch is recorded as advisory evidence instead of silently rewritten. Missing effective-host evidence stays unavailable and is never inferred from the registry. | ||
| routing declared: frontender (work_type=frontend) | ||
| run run-bc0994cb profile driver (differs from declared route; advisory) | ||
| ``` | ||
| 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. | ||
| ## The whole model in one line each | ||
| 1. **Declare** the pool once: `planr agents init` (flags, wizard, or the plain scaffold). | ||
| 2. **Tag** items with their use case: `--work-type frontend|backend|...` — free-form. | ||
| 3. **Pick** routes: every packet carries profile, model, skill, and fallbacks. | ||
| 4. **Deliver** with `--profile` as part of the evidence. | ||
| 5. **Trace** proves declared vs. actual — silent overrides get caught. | ||
| 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). | ||
| 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). |
@@ -23,8 +23,2 @@ { | ||
| "planr_agents_list", | ||
| "planr_presets_list", | ||
| "planr_preset_evaluate", | ||
| "planr_preset_apply", | ||
| "planr_preset_registry_verify", | ||
| "planr_preset_registry_import", | ||
| "planr_preset_registry_list", | ||
| "planr_policy_show", | ||
@@ -113,7 +107,5 @@ "planr_policy_check", | ||
| "planr agents check", | ||
| "planr agents preset list", | ||
| "planr agents preset apply", | ||
| "planr agents preset registry verify", | ||
| "planr agents preset registry import", | ||
| "planr agents preset registry list", | ||
| "planr routing bundle inspect", | ||
| "planr routing bundle preview", | ||
| "planr routing bundle apply", | ||
| "planr item route", | ||
@@ -120,0 +112,0 @@ "planr trace item", |
+8
-25
@@ -90,3 +90,3 @@ # Long-Running Goals | ||
| The recommended combination. Install the plugin and provision the subagent roles once: | ||
| Install the plugin, initialize the repository, and optionally apply a repository routing bundle: | ||
@@ -96,3 +96,6 @@ ```bash | ||
| codex plugin add planr@planr | ||
| planr project init "My Product" --client codex # writes .codex/agents/planr-worker.toml + planr-reviewer.toml | ||
| planr project init "My Product" | ||
| planr-routing compile balanced --host codex-openai --output routing-bundle.json | ||
| planr routing bundle preview routing-bundle.json | ||
| planr routing bundle apply routing-bundle.json | ||
| ``` | ||
@@ -107,3 +110,3 @@ | ||
| 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). | ||
| 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. | ||
@@ -148,26 +151,6 @@ ### Claude Code | ||
| A goal run has three roles with different intelligence needs, so they should not all run on the same model tier: | ||
| 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. | ||
| - **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. | ||
| - **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. | ||
| - **Reviewer**: the truth gate. It inherits the driver's model on purpose — make workers cheap, not the verdict. | ||
| 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). | ||
| Where each host configures the worker tier (the shipped role files carry these defaults): | ||
| | Host | Driver | Worker | Configured in | | ||
| | --- | --- | --- | --- | | ||
| | Codex | session default (e.g. `gpt-5.5` at `high`) | `model = "gpt-5.5"`, `model_reasoning_effort = "medium"` | `.codex/agents/planr-worker.toml` | | ||
| | Claude Code | session model (e.g. `fable` at `high` via `/model` + `/effort`) | `model: opus`, `effort: medium` | `planr-worker.md` frontmatter | | ||
| | Cursor | chat model of the driving session | `model: inherit` by default; pin a cheaper Cursor model id | `.cursor/agents/planr-worker.md` frontmatter | | ||
| 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. | ||
| 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. | ||
| 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: | ||
| - **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. | ||
| - **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`. | ||
| - **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. | ||
| ## Coming From Other Goal Tools | ||
@@ -174,0 +157,0 @@ |
+2
-2
@@ -75,3 +75,3 @@ # Install Planr | ||
| `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. | ||
| `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. | ||
@@ -91,3 +91,3 @@ 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. | ||
| 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. | ||
| 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). | ||
@@ -94,0 +94,0 @@ ## From Source |
@@ -45,6 +45,3 @@ # MCP Contract | ||
| - trace item, log add, and log read (including three-stage route observations) | ||
| - built-in policy/binding catalog listing plus safe-pack/custom composition status | ||
| - declarative registry verification with canonical evaluation/safe-binding gates, preview-first immutable import, and manifest-anchored integrity/signature/freshness-checked offline cache listing | ||
| - policy preset preview/apply by path or built-in id with repository-only target validation and deterministic provenance lock | ||
| - deterministic offline preset simulation plus explicit opt-in live-host execution with Planr-controlled challenge workspaces, strict task artifacts read and hashed by Planr, candidate/task outcome oracles, failed-live-attempt `unverified`/incomplete lifecycle semantics, production policy-capability checks, estimated arbitrary-process claims, optional Ed25519-verified run/suite/time/task/challenge-bound telemetry that alone can promote effective route and usage evidence to trusted/recommendation-eligible, observed process latency, transition/correction/violation counts, result hashes, and deterministic lifecycle thresholds | ||
| - provider-neutral RoutingBundle v1 inspection, repository-safe preview/apply, and durable application evidence | ||
| - review annotate, ingest, artifact, evidence, and close | ||
@@ -71,3 +68,3 @@ - item close, context create, and search | ||
| - Codex: `.planr/integrations/codex-mcp.toml` plus `.codex/agents/` roles | ||
| - Codex: `.planr/integrations/codex-mcp.toml`; optional `.codex/agents/` roles are routing-bundle artifacts | ||
| - Claude Code: `.mcp.json` plus `.claude/agents/` roles | ||
@@ -74,0 +71,0 @@ - Cursor: `.cursor/mcp.json` plus `.cursor/agents/` roles and `.cursor/skills/` skill copies |
+20
-177
@@ -1,196 +0,39 @@ | ||
| # Model Routing | ||
| # Model routing | ||
| 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. | ||
| 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. | ||
| 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. | ||
| 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. | ||
| 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. | ||
| ## Quick Start | ||
| 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: | ||
| ```bash | ||
| planr agents init # writes .planr/agents.toml; never overwrites without --force | ||
| ``` | ||
| Or declare `.planr/agents.toml` by hand: | ||
| ```toml | ||
| [profiles.fable-driver] | ||
| client = "cursor" | ||
| model = "fable-5" | ||
| [profiles.worker] | ||
| client = "host-a" | ||
| model = "model-id" | ||
| agent_type = "repository-role" | ||
| effort = "high" | ||
| cost_tier = "premium" | ||
| capabilities = ["orchestration", "review", "planning"] | ||
| notes = "Planner/architect and judge. Verdicts stay on this tier." | ||
| skill = "planr-work" | ||
| [profiles.gpt55-coder] | ||
| client = "codex" | ||
| model = "gpt-5.5" | ||
| effort = "xhigh" | ||
| cost_tier = "standard" | ||
| capabilities = ["code", "steerable"] | ||
| notes = "Primary implementer: strong, fast, cheap on subscription." | ||
| [[routes]] | ||
| match = { work_type = "code" } | ||
| profile = "gpt55-coder" | ||
| fallbacks = ["fable-driver"] | ||
| [[routes]] | ||
| match = { work_type = "review" } | ||
| profile = "fable-driver" | ||
| [route_default] | ||
| profile = "gpt55-coder" | ||
| fallbacks = ["fable-driver"] | ||
| profile = "worker" | ||
| ``` | ||
| Validate and inspect: | ||
| Create a neutral scaffold or an explicit registry: | ||
| ```bash | ||
| planr agents check # non-zero exit only on parse failure; warnings pass | ||
| planr agents list # resolved profiles, routes, and warnings | ||
| planr agents init | ||
| planr agents init --profile worker=host-a/model-id@high#standard --route code=worker | ||
| planr agents check | ||
| planr agents list --json | ||
| ``` | ||
| From then on, every pick carries the recommendation: | ||
| 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. | ||
| ```bash | ||
| planr pick --json | ||
| ``` | ||
| Workers may report observed routing with logs and route-audit evidence. Requested-only values never become effective proof; missing effective evidence remains explicitly unavailable. | ||
| ```json | ||
| "routing": { | ||
| "profile": "gpt55-coder", | ||
| "client": "codex", | ||
| "model": "gpt-5.5", | ||
| "effort": "xhigh", | ||
| "cost_tier": "standard", | ||
| "fallbacks": ["fable-driver"], | ||
| "matched_selector": "work_type=code" | ||
| } | ||
| ``` | ||
| 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: | ||
| 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. | ||
| ## The Registry File | ||
| `.planr/agents.toml` has three parts: | ||
| - `[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. | ||
| - `[[routes]]` — `match` selects work (`work_type = "code"` or `plan = "pln-1234abcd"`), `profile` names the primary, `fallbacks` the ordered alternatives. | ||
| - `[route_default]` — catches everything no route matched. | ||
| 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`). | ||
| ## Per-Item Overrides | ||
| 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: | ||
| ```bash | ||
| planr item route <item-id> # resolved route + source: override or policy | ||
| planr item route <item-id> --set fable-driver # pin; validates the profile id, emits route_overridden | ||
| planr item route <item-id> --clear # unpin; policy applies again, emits route_override_cleared | ||
| planr-routing compile balanced --host codex-openai --output routing-bundle.json | ||
| planr routing bundle preview routing-bundle.json | ||
| planr routing bundle apply routing-bundle.json | ||
| ``` | ||
| 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. | ||
| 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). | ||
| ## Host-Native Rendering | ||
| 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. | ||
| Two safety rules keep this predictable: | ||
| - **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. | ||
| - **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. | ||
| Without a registry, installs write the static role files byte-identically to previous releases. | ||
| ## Prompt Routing | ||
| `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. | ||
| ## Run Audit | ||
| 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 at two levels: workers report the profile they actually ran on via `planr log add`/`planr done --profile <id>` (or `PLANR_PROFILE`) for backward-compatible mismatch checks, and can attach a strict `--route-audit <observation.json>` that keeps requested, host-resolved, and effective model/effort/fork values separate. Every dimension carries enforcement confidence and a constrained evidence source; missing host evidence stays unavailable instead of inheriting the request. | ||
| - `planr trace item <id>` (MCP: `planr_trace_item`) shows the declared route next to every run's actual client/profile and three-stage observation with a `mismatch` marker. | ||
| - 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. | ||
| - `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). | ||
| - `planr export`/`import` carry the registry with the package, preview-first; an existing registry at the destination is never silently overwritten. | ||
| 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. | ||
| 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. | ||
| 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. | ||
| ## Failure Behavior | ||
| - **No registry file**: nothing changes. Pick packets simply have no `routing` key. | ||
| - **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. | ||
| - **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. | ||
| - Never put credentials in the registry — it holds configuration strings only, and secret-like values are flagged. | ||
| ## Use-Case Pools | ||
| 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: | ||
| ```toml | ||
| [profiles.designer] | ||
| client = "claude-code" | ||
| model = "opus" | ||
| effort = "high" | ||
| cost_tier = "premium" | ||
| skill = "frontend-design" # dispatch this profile *with* this skill | ||
| [profiles.backender] | ||
| client = "codex" | ||
| model = "gpt-5.5" | ||
| effort = "xhigh" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| [[routes]] | ||
| match = { work_type = "frontend" } | ||
| profile = "designer" | ||
| fallbacks = ["driver"] | ||
| [[routes]] | ||
| match = { work_type = "design" } | ||
| profile = "designer" | ||
| [[routes]] | ||
| match = { work_type = "backend" } | ||
| profile = "backender" | ||
| fallbacks = ["driver"] | ||
| ``` | ||
| 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. | ||
| 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. | ||
| 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). | ||
| ## Host Matrix | ||
| Where each host reads its model configuration from, and what silently defeats a pin there (state of July 2026): | ||
| | Host | Native mechanism | Rendered by `planr install`? | Silent overrides / traps | | ||
| | --- | --- | --- | --- | | ||
| | 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 | | ||
| | 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 | | ||
| | 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 | | ||
| | 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)) | | ||
| | 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 | | ||
| 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. | ||
| ## Command Summary | ||
| 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. | ||
| Bundle application is restricted to the repository. Planr never edits user configuration such as `~/.codex/config.toml`. See [Routing Bundles](ROUTING_BUNDLES.md). |
+5
-3
@@ -5,3 +5,3 @@ # Planr Skills | ||
| 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`). | ||
| 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`). | ||
@@ -169,6 +169,8 @@ ## Install As Plugin (preferred) | ||
| planr project init "My Product" --client all # writes .codex/agents/*.toml, .claude/agents/*.md, and .cursor/agents/*.md | ||
| planr install codex # provisions roles for an existing project (same for claude and cursor) | ||
| planr agents init # generates canonical native Codex roles in an existing project | ||
| planr install claude # provisions Claude's independent roles | ||
| planr install cursor # provisions Cursor's independent roles and skills | ||
| ``` | ||
| 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. | ||
| 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. | ||
@@ -175,0 +177,0 @@ 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. |
+2
-14
| { | ||
| "name": "planr", | ||
| "version": "1.4.0", | ||
| "version": "1.5.0", | ||
| "description": "Local-first planning and execution coordination for coding agents.", | ||
@@ -18,5 +18,2 @@ "license": "MIT", | ||
| "plugins", | ||
| "presets", | ||
| "evaluations", | ||
| "website", | ||
| "README.md", | ||
@@ -27,16 +24,7 @@ "LICENSE.md", | ||
| "scripts": { | ||
| "alchemy:check-runtime": "node scripts/check-alchemy-runtime.mjs", | ||
| "build:native": "cargo build --release", | ||
| "test": "cargo test", | ||
| "pack:check": "npm pack --dry-run", | ||
| "deploy:test": "node scripts/cloudflare-test.mjs deploy", | ||
| "destroy:test": "node scripts/cloudflare-test.mjs destroy", | ||
| "site:build": "node scripts/build-site.mjs", | ||
| "site:check": "pnpm site:test && pnpm site:build", | ||
| "site:test": "node --test website/*.test.mjs", | ||
| "site:serve": "node website/serve.mjs" | ||
| "routing": "cargo run --manifest-path planr-routing/Cargo.toml --" | ||
| }, | ||
| "devDependencies": { | ||
| "alchemy": "0.93.7" | ||
| }, | ||
| "engines": { | ||
@@ -43,0 +31,0 @@ "node": ">=18" |
| { | ||
| "name": "planr", | ||
| "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.", | ||
| "version": "1.4.0", | ||
| "version": "1.5.0", | ||
| "author": { | ||
@@ -6,0 +6,0 @@ "name": "instructa" |
| { | ||
| "name": "planr", | ||
| "version": "1.4.0", | ||
| "version": "1.5.0", | ||
| "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,0 +5,0 @@ "author": { |
@@ -70,3 +70,3 @@ --- | ||
| - 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`. | ||
| - 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. | ||
| - 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. | ||
@@ -73,0 +73,0 @@ - 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. |
+9
-56
@@ -25,57 +25,2 @@ # Planr | ||
| ## New in 1.4.0: Verified Presets & Catalog | ||
| Planr now ships a verified preset system for composing a provider-neutral usage policy with a host binding, previewing every repository-local change, and applying the result only after confirmation: | ||
| ```bash | ||
| planr agents preset list | ||
| planr agents preset apply balanced --binding codex-openai --preview | ||
| planr agents preset apply balanced --binding codex-openai --confirm | ||
| ``` | ||
| The built-in catalog includes four policies, five host bindings, and 20 declared safe pairs. Reproducible evaluation, signed registry verification, and the public [Planr Preset Catalog](https://planr-test-catalog.office-35d.workers.dev/) keep recommendation evidence inspectable without making the registry a runtime dependency. Full guides: [Preset Composition](docs/PRESET_COMPOSITION.md) · [Preset Evaluation](docs/PRESET_EVALUATION.md) · [Preset Registry](docs/PRESET_REGISTRY.md). | ||
| ## New in 1.3.0: Native Host Hooks | ||
| `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: | ||
| ```text | ||
| ## planr state | ||
| project: Hookboard | map: 5/5 settled | 0 ready, 0 picked, 0 in_review | ||
| goal contract: DONE when every in-scope map item is closed with log evidence, ... | ||
| routing: registry active (3 profiles; pick packets carry model routing) | ||
| next: planr plan audit pln-fc584c28 --json | ||
| ``` | ||
| - **`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. | ||
| - **Loop recovery becomes mechanism, not discipline** — an agent that restarts or compacts mid-loop picks up exactly where the map says it left off. | ||
| - **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. | ||
| - **Fail-open and additive** — hooks never block a session (10s timeout, always exit 0), existing hook files are merged, `--no-hooks` opts out. | ||
| Full guide: [Hooks](docs/HOOKS.md) · [Release notes](https://github.com/instructa/planr/releases/tag/v1.3.0). | ||
| ## Model Routing (1.2.0) | ||
| Declare once which model handles which work — every task then carries its own routing, and your agents delegate automatically: | ||
| ```toml | ||
| # .planr/agents.toml (write it with `planr agents init`) | ||
| [profiles.frontender] | ||
| client = "cursor" | ||
| model = "opus" | ||
| skill = "frontend-design" | ||
| [[routes]] | ||
| match = { work_type = "frontend" } | ||
| profile = "frontender" | ||
| fallbacks = ["driver"] | ||
| ``` | ||
| - **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. | ||
| - **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. | ||
| - **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. | ||
| - **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`. | ||
| 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). | ||
| ## Install | ||
@@ -109,3 +54,3 @@ | ||
| 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. | ||
| 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. | ||
@@ -191,2 +136,10 @@ <a id="install-plugin-codex"></a> | ||
| ## What's new | ||
| - **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). | ||
| - **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). | ||
| - **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). | ||
| For the complete release history, see the [Changelog](CHANGELOG.md). | ||
| ## Docs | ||
@@ -193,0 +146,0 @@ |
| # Preset Composition | ||
| Planr composes two independent inputs: | ||
| - a provider-neutral Usage Policy v1 TOML file; | ||
| - a versioned host binding containing exact client/model/effort and dispatch capabilities. | ||
| Preview is the default: | ||
| ```bash | ||
| planr agents preset apply ./policy.toml --binding ./codex-binding.toml --preview --json | ||
| planr agents preset apply ./policy.toml --binding ./codex-binding.toml --confirm --json | ||
| ``` | ||
| The equivalent MCP tool is `planr_preset_apply` with `policy`, `binding`, and optional `confirm` fields. Both surfaces call one application service and return the same compatibility, permission, verification-age, provenance, conflict, and artifact shapes. | ||
| ## Built-in Catalog and Safe Packs | ||
| `planr agents preset list --json` (MCP: `planr_presets_list`) lists the four embedded policy presets, five host bindings, source checksums, and 20 declared safe pairs. Built-ins resolve by id or id plus `.toml`, so they work from any repository without copying package files first: | ||
| ```bash | ||
| planr agents preset apply balanced --binding codex-openai --preview --json | ||
| planr agents preset apply read-only-audit --binding mixed-host --confirm --json | ||
| ``` | ||
| Policies: `balanced`, `low-usage`, `max-quality`, and `read-only-audit`. Bindings: `codex-openai`, `cursor-openai`, `cursor-fable-grok`, `claude-native`, and `mixed-host`. The exact inspectable sources ship under `presets/policies/` and `presets/bindings/` and are also compiled into the binary. | ||
| A response whose inputs came from the embedded catalog carries `pack.status = "safe"` and `pack.safe = true`. Explicit file inputs remain custom even when their internal ids match a built-in; they are allowed, carry `pack.status = "custom"`, and add a warning requiring review of compatibility, permission, and artifact diffs. This prevents copied or modified files from inheriting safe-pack status by name alone. | ||
| ## Host Binding v1 | ||
| Bindings use strict TOML: unknown fields fail parsing. Abstract role keys must cover every role in `policy.execution.roles`; bindings may add a driver profile without adding it to execution permissions. | ||
| ```toml | ||
| schema_version = 1 | ||
| id = "codex-team" | ||
| version = "1.0.0" | ||
| host = "codex" | ||
| driver_role = "driver" | ||
| default_role = "driver" | ||
| capability_evidence = ["codex-0.138-cross-tier-smoke"] | ||
| billing_assumptions = ["local subscription"] | ||
| known_limitations = ["effective model requires host evidence"] | ||
| [capabilities] | ||
| model_override = true | ||
| effort_override = true | ||
| fork_none = true | ||
| fork_all = true | ||
| max_partial_fork_turns = 4 | ||
| [profiles.driver] | ||
| profile = "sol" | ||
| client = "codex" | ||
| model = "gpt-5.5" | ||
| effort = "xhigh" | ||
| [profiles.worker] | ||
| profile = "luna" | ||
| client = "codex" | ||
| model = "gpt-5.4-mini" | ||
| effort = "high" | ||
| skill = "planr-work" | ||
| # Omit for the deterministic `none` default. | ||
| fork_turns = { mode = "partial", turns = 2 } | ||
| [[routes]] | ||
| work_type = "code" | ||
| role = "worker" | ||
| fallback_roles = ["driver"] | ||
| [verification] | ||
| id = "verify-codex-team" | ||
| verified_at_unix = 1900000000 | ||
| max_age_seconds = 2592000 | ||
| [[artifacts]] | ||
| path = ".codex/agents/luna.toml" | ||
| kind = "codex_agent" | ||
| content = '''model = "gpt-5.4-mini" | ||
| model_reasoning_effort = "high" | ||
| ''' | ||
| ``` | ||
| Codex cross-tier children never accept `fork_turns = { mode = "all" }`, because full history inheritance defeats the requested model/effort override. Omitted fork configuration defaults to `none`. A partial fork must be positive, at or below `max_partial_fork_turns`, and backed by non-empty `capability_evidence`. Secret-like values are forbidden in all binding metadata, including capability evidence, billing assumptions, limitations, profile metadata, routes, and artifact paths/kinds; composition rejects them with field-only diagnostics before producing dispatch/warning output or mutating the repository. | ||
| ## Preview and Lock | ||
| The preview includes: | ||
| - composed abstract-role to registry-profile and dispatch-context mappings; | ||
| - compatibility errors and host/billing limitations; | ||
| - execution-permission additions relative to the active policy; | ||
| - verification id, age, maximum age, and fresh/stale state; | ||
| - source ids, versions, SHA-256 hashes, Planr version, and applied-at value; | ||
| - every target's kind, size, proposed/existing hash, `create`, `unchanged`, or `conflict` action, and deterministic old/proposed configuration. TOML is projected structurally so policy limits/transitions and registry routes/profiles/fallbacks are directly auditable; other text is line-projected. Secret-like keys and credential-shaped values are replaced with `[REDACTED]`. | ||
| Confirmed apply writes the previewed bytes and records `policy_applied`. The lock at `.planr/preset.lock.toml` contains source hashes and hashes for every generated non-lock artifact. Tests and reproducible packaging can pin `PLANR_PRESET_NOW_UNIX` and `PLANR_PRESET_APPLIED_AT`; ordinary operation records current time. | ||
| ## Repository Boundary | ||
| Only these repository-relative targets are valid: | ||
| - `.planr/policy.toml` | ||
| - `.planr/agents.toml` | ||
| - `.planr/preset.lock.toml` | ||
| - files below `.codex/{agents,skills}/`, `.claude/{agents,skills}/`, or `.cursor/{agents,skills}/` | ||
| Planr rejects absolute paths, `.`/`..` traversal, non-normalized paths, symlink crossings, home/global targets, and repository `.codex/config.toml` before the first write. Existing different files are conflicts and are never overwritten; there is no force or privileged mode. |
| # Preset Evaluation | ||
| Planr provides three explicit evidence levels over the same versioned fixture and lifecycle engine: | ||
| - the default offline policy simulation performs no external execution and can reach `verified` but never `recommended`; | ||
| - the opt-in live-host runner executes every task through a user-selected local adapter and evaluates Planr-owned workspace artifacts, but arbitrary-process route and cost claims cannot reach `recommended` without independent instrumentation. | ||
| - the instrumented live-host runner joins those artifacts to Ed25519-signed telemetry receipts and can reach `recommended` when trusted route, usage, and all other thresholds pass. | ||
| `evaluations/preset-suite-v1.toml` defines distinct inputs, artifact kinds, and candidate/task-bound output oracles for exploration, implementation, mechanical, browser, visual, security, and subagent workflows. Input and result artifacts are SHA-256 hashed. | ||
| ```sh | ||
| planr agents preset evaluate | ||
| planr --json agents preset evaluate --at-unix 1783987200 | ||
| planr agents preset evaluate --report-dir reports/preset-v1 | ||
| planr agents preset evaluate \ | ||
| --live-host-command /absolute/path/to/host-adapter \ | ||
| --live-host-arg optional-argument \ | ||
| --trusted-telemetry-signer production-collector \ | ||
| --trusted-telemetry-collector /absolute/path/to/collector | ||
| ``` | ||
| Evaluation defaults to current wall-clock time, so evidence becomes `stale` after fixture expiry. `--at-unix` is the deterministic override for tests and reproducible exports. `--host` requires binding compatibility. The signer and collector flags are all-or-none and require live mode. MCP exposes the equivalent optional `at_unix`, `host`, `live_host_command`, `live_host_args`, `trusted_telemetry_signer`, and `trusted_telemetry_collector` inputs. | ||
| ## Offline simulation | ||
| Offline artifacts say `evidence_scope = "policy_simulation"`, `task_executed = false`, `outcome_oracle_evaluated = false`, and `recommendation_eligible = false`. Projected tool, token, credit, and latency values carry `metrics_source = "estimated_projection"` and `metering_confidence = "estimated"`. | ||
| Binding configuration populates only requested and resolved route stages. Effective model, effort, and context-fork dimensions remain `unavailable`, so route coverage is zero and the recommendation list is empty. Offline `verified` means only that policy composition, transition simulation, and hashes are complete. | ||
| ## Explicit live-host runner | ||
| `--live-host-command` must be an absolute executable path. Planr starts it once per candidate/task fixture, writes one JSON request to stdin, waits for process completion, measures elapsed time, and parses one JSON response from stdout. This is an explicit arbitrary-process boundary: only run a trusted adapter. No live process is started without the flag. | ||
| For each run, Planr creates a Planr-controlled temporary workspace containing `challenge.json`. The request contains schema/suite/candidate/policy/binding/task versions, task kind, input, input hash, required artifact kind, and absolute workspace/challenge/artifact paths. It deliberately does not expose the expected output or challenge contents. The adapter must read the challenge, write a strict schema-v1 artifact to `artifact_path`, hash those exact bytes, and bind its response back to that artifact: | ||
| ```json | ||
| { | ||
| "schema_version": 1, | ||
| "host_id": "example-host", | ||
| "host_version": "1.0.0", | ||
| "candidate_id": "balanced-codex-openai", | ||
| "task_id": "browser-report-smoke", | ||
| "input_sha256": "<request input hash>", | ||
| "artifact_kind": "browser_trace", | ||
| "artifact_sha256": "<SHA-256 of the artifact file>", | ||
| "output": "balanced-codex-openai:browser-report-smoke:browser-report-inspected", | ||
| "effective_model": "model-id", | ||
| "effective_effort": "high", | ||
| "effective_context_fork": { "mode": "none" }, | ||
| "tool_calls": 1, | ||
| "tokens": 10, | ||
| "credits_micros": 100, | ||
| "retries": 1, | ||
| "availability_fallbacks": 1, | ||
| "quality_escalations": 1, | ||
| "corrections": 0, | ||
| "violations": 0 | ||
| } | ||
| ``` | ||
| The artifact file is separate from stdout and has this strict shape: | ||
| ```json | ||
| { | ||
| "schema_version": 1, | ||
| "candidate_id": "balanced-codex-openai", | ||
| "task_id": "browser-report-smoke", | ||
| "input_sha256": "<request input hash>", | ||
| "artifact_kind": "browser_trace", | ||
| "challenge_sha256": "<SHA-256 of Planr's challenge file>", | ||
| "output": "balanced-codex-openai:browser-report-smoke:browser-report-inspected" | ||
| } | ||
| ``` | ||
| Planr reads the artifact itself after the process exits, rejects missing, symlinked, oversized, malformed, or extra-field artifacts, computes its SHA-256 independently, verifies candidate/task/input/artifact and challenge binding, and finally evaluates the versioned task oracle. Neither a constant responder nor a request-aware mapper that only echoes dynamic request fields and public success labels writes this Planr-controlled artifact, so both have `task_executed = false`, `evidence_complete = false`, status `unverified`, and cannot recommend. A wholly failed live attempt also sets the report-level `reproducible_evidence = false`; merely attempting the adapter is never described as evaluated live evidence. | ||
| Production policy checks remain authoritative: tool-budget and write-capability checks are evaluated from the candidate policy, never adapter claims. A read-only candidate fails the implementation and mechanical write fixtures even if the adapter returns otherwise successful JSON. | ||
| The adapter reports effective route and usage values, but process exit does not make those claims trusted. Effective route fields are labeled `host_report` with `estimated` enforcement, so they do not increment `verified_route_runs`. Tool/token/credit metering is also `estimated`; only elapsed process latency is independently measured by Planr. Retry/fallback/escalation/correction/violation counts remain visible host reports. The process-adapter boundary therefore always sets `recommendation_eligible = false`, even when every workspace artifact passes. | ||
| ## Independently signed telemetry | ||
| Recommendation-capable live evaluation uses a separately provisioned telemetry collector. The repository trust registry at `.planr/trusted-telemetry.toml` pins both an Ed25519 signer key and the SHA-256 of the collector executable; the evaluation invocation supplies only the registry signer id and the absolute collector path. A caller-selected public key, run id, or precomputed receipt bundle is not accepted. The registry shape is: | ||
| ```toml | ||
| schema_version = 1 | ||
| [[signers]] | ||
| id = "production-collector" | ||
| public_key_hex = "<64-hex-character-ed25519-public-key>" | ||
| collector_sha256 = "<64-hex-character-executable-sha256>" | ||
| ``` | ||
| Planr creates a fresh run UUID before evaluation and a fresh challenge nonce for every task. Only after the live adapter exits and Planr reads and hashes the workspace artifact does it invoke the hash-pinned collector. The collector receives a strict JSON identity request containing the Planr-owned run id, evaluation and suite identity, candidate/task/input/artifact identity, exact artifact SHA-256, and challenge nonce. It independently adds host route, usage, transition, correction, and violation measurements, then returns `{ "payload": ..., "signature_hex": "..." }`. The adapter never receives a signing key, and pre-run receipts cannot know the fresh run/challenge/artifact tuple. | ||
| Before invocation Planr verifies that the collector is a regular absolute-path file whose SHA-256 matches the selected registry entry, and it rechecks the digest for every task. It rejects the trust upgrade for a failed collector, malformed response, bad signature, wrong run/suite/time, wrong task/input/artifact/challenge binding, blank host identity, or host identity mismatch with the live adapter response. Missing or rejected receipts do not abort artifact evaluation: that result falls back to `host_reported`/`estimated`, gets zero verified route credit, and is recommendation-ineligible. Registry, signer, and collector configuration errors fail the evaluation before execution. | ||
| For a verified post-run receipt, Planr uses only signed route, usage, transition, correction, and violation values. Effective route dimensions carry `evidence = "telemetry_receipt"` and `enforcement = "verified"`; tool/token/credit dimensions and result `metering_confidence` are `trusted`; process latency remains Planr-observed. These results use `metrics_source = "trusted_telemetry"` and may satisfy recommendation gates. | ||
| ## Lifecycle and thresholds | ||
| - `recommended`: every task artifact and oracle passes, every effective route and usage measurement has a valid signed telemetry receipt, and all thresholds pass; | ||
| - `verified`: offline simulation is complete, or every required live workspace artifact and outcome oracle passed but recommendation-grade instrumentation is unavailable or a threshold failed; | ||
| - `stale`: evaluation time is later than fixture expiry; | ||
| - `incompatible`: requested host is unsupported by the binding; | ||
| - `unverified`: required composition, transition, hash, live task execution, challenge-bound artifact, or outcome-oracle evidence is incomplete. | ||
| Only independently instrumented live task evidence can set `recommendation_evidence_complete = true`. Hashing simulator-authored JSON, process stdout, or an adapter-authored route/cost claim cannot promote it. | ||
| ## Immutable output | ||
| `--report-dir` accepts only a normalized repository-relative directory and rejects absolute paths, traversal, non-directory components, and symlink crossings. It creates `verification.json` and `report.md` without overwrite; either existing target fails the command. | ||
| The report includes suite/Planr/configured-model provenance. Live results additionally include host id/version, Planr-read workspace artifact and oracle hashes, host route/usage evidence with explicit estimated or trusted provenance, observed process latency, corrections/transitions/violations, policy-capability checks, and result hashes. | ||
| ## Sol/Luna Codex contract | ||
| `evaluations/sol-luna-codex-v1.toml` independently proves that cross-tier `fork_turns = "all"` is rejected, `none` preserves Luna parameters, missing effective evidence cannot route-verify, and process-evidenced effective model/effort/fork values can verify. |
| # Preset Registry | ||
| Planr's registry is an optional distribution boundary, never a runtime dependency. Active projects keep using their repository-local `.planr/policy.toml`, `.planr/agents.toml`, and preset lock when the registry is unavailable. Previously imported packs remain available under `.planr/registry/cache/` for deterministic offline use. | ||
| ## Trust and integrity | ||
| A schema-v1 registry manifest identifies a registry version and one or more versioned entries. Each entry declares: | ||
| - kind (`policy`, `host-binding`, or `pack`), lifecycle (`published`, `deprecated`, or `revoked`), and evaluation status; | ||
| - Planr version bounds and compatible hosts; | ||
| - verification and review timestamps, a verification artifact, and optional replacement/revocation metadata; | ||
| - for `verified` or `recommended` entries, the exact policy, binding, and evaluation-suite ids and versions covered by that evidence; | ||
| - the normalized path, declarative kind, byte length, and SHA-256 digest of every artifact; | ||
| - an optional Ed25519 signature. | ||
| Signatures do not establish their own trust. Planr verifies them only against keys provisioned separately in `.planr/registry/trusted-maintainers.toml` (or an explicitly selected trust store): | ||
| ```toml | ||
| schema_version = 1 | ||
| [[maintainers]] | ||
| id = "planr-maintainers" | ||
| public_key = "<64 hexadecimal Ed25519 public-key characters>" | ||
| revoked = false | ||
| ``` | ||
| An unsigned pack can be checksum-verified, but a manifest's `recommended` claim is demoted to `verified` unless a non-revoked pinned maintainer signature verifies. A trusted signature is necessary but not sufficient: Planr also re-runs canonical policy/binding composition and safe-artifact validation, binds the shipped bytes to the current built-in evaluation inputs, and validates the current suite/task provenance. Candidate metrics and threshold gates are derived again from the task results; result hashes, task-oracle coverage, the canonical Codex dispatch contract, trusted route/metering evidence, and the report recommendation record must all agree before `recommended` survives. A merely signed status label or stale suite therefore cannot promote an entry. | ||
| Invalid signatures, revoked entries/signers, incompatible Planr/host constraints, checksum/size mismatches, traversal, symlinks, binary or executable content, secret-like content, semantically invalid bindings, and malformed declarative artifacts fail closed. Every registry policy, including an `experimental` entry without evaluation metadata, uses the same public-distribution safety check: commands, hooks, environment grants, network/MCP access, secret references, and overwrite permission are rejected. Manifest ids, versions, hosts, references, reasons, paths, and signer metadata are screened field-by-field for secret-like values before they can reach diagnostics; public keys and signature bytes are shape-validated but deliberately not classified as secrets. Stale verification or deprecation remains visible and removes recommendation. | ||
| ## Verification and offline import | ||
| Remote retrieval is deliberately outside the command. Downloading a manifest and its content is an explicit operator action; Planr then verifies local inputs: | ||
| ```sh | ||
| planr agents preset registry verify registry.toml \ | ||
| --entry balanced-codex \ | ||
| --content-root ./download \ | ||
| --host codex | ||
| ``` | ||
| `--host` may be omitted only for entries whose `compatible_hosts` list is empty. A constrained entry with no explicit host fails closed. | ||
| Import is preview-first. The preview lists exactly the manifest-declared files and deterministic cache target. `--confirm` copies only those verified files into a manifest-hash-addressed immutable directory and atomically stores both the original registry manifest and a cache receipt: | ||
| ```sh | ||
| planr agents preset registry import registry.toml \ | ||
| --entry balanced-codex \ | ||
| --content-root ./download \ | ||
| --host codex | ||
| planr agents preset registry import registry.toml \ | ||
| --entry balanced-codex \ | ||
| --content-root ./download \ | ||
| --host codex \ | ||
| --confirm | ||
| ``` | ||
| `planr agents preset registry list` requires no source manifest or network. The mutable receipt is inventory metadata, not a checksum or trust authority. Every read binds the cache path to the stored manifest hash, re-verifies the original manifest and maintainer signature against the repository trust store, compares receipt inventory with the manifest, and recomputes artifact sizes and hashes from the manifest. Coordinated edits to content and receipt therefore remain detectable. The command reports `current` or `stale` freshness from the review timestamp and marks malformed or tampered cache entries unusable. Extra source files are never imported. | ||
| The equivalent MCP tools are `planr_preset_registry_verify`, `planr_preset_registry_import`, and `planr_preset_registry_list`. MCP import also previews unless `confirm` is true. |
| schema_version = 1 | ||
| id = "planr-preset-suite" | ||
| version = "1.8.0" | ||
| verified_at_unix = 1783987200 | ||
| expires_at_unix = 1815523200 | ||
| [thresholds] | ||
| minimum_runs = 7 | ||
| minimum_quality_score_bps = 8500 | ||
| minimum_reliability_bps = 9000 | ||
| maximum_average_credits_micros = 1200000 | ||
| maximum_p95_latency_ms = 5000 | ||
| maximum_transition_contract_failures = 0 | ||
| maximum_safety_stop_failures = 0 | ||
| require_verified_routes = true | ||
| require_result_hashes = true | ||
| [[candidates]] | ||
| id = "balanced-codex-openai" | ||
| policy = "balanced" | ||
| binding = "codex-openai" | ||
| [[candidates]] | ||
| id = "low-usage-codex-openai" | ||
| policy = "low-usage" | ||
| binding = "codex-openai" | ||
| [[candidates]] | ||
| id = "max-quality-codex-openai" | ||
| policy = "max-quality" | ||
| binding = "codex-openai" | ||
| [[candidates]] | ||
| id = "read-only-audit-codex-openai" | ||
| policy = "read-only-audit" | ||
| binding = "codex-openai" | ||
| [[tasks]] | ||
| id = "explore-routing-boundaries" | ||
| version = "1.0.0" | ||
| kind = "exploration" | ||
| objective = "Inspect policy and host routing boundaries without mutation." | ||
| input = "inspect-routing-boundaries" | ||
| artifact_kind = "inspection_manifest" | ||
| expected_output = "routing-boundaries-inspected" | ||
| work_units = 8 | ||
| transition = "retry" | ||
| requires_write = false | ||
| minimum_tool_budget = 1 | ||
| [[tasks]] | ||
| id = "implement-bounded-policy-change" | ||
| version = "1.0.0" | ||
| kind = "implementation" | ||
| objective = "Implement a bounded policy change and preserve verification evidence." | ||
| input = "implement-bounded-policy-change" | ||
| artifact_kind = "patch_manifest" | ||
| expected_output = "bounded-policy-change-implemented" | ||
| work_units = 14 | ||
| transition = "quality_escalation" | ||
| requires_write = true | ||
| minimum_tool_budget = 80 | ||
| [[tasks]] | ||
| id = "mechanical-schema-rewrite" | ||
| version = "1.0.0" | ||
| kind = "mechanical" | ||
| objective = "Apply a deterministic schema rewrite across owned files." | ||
| input = "rewrite-owned-schema" | ||
| artifact_kind = "rewrite_manifest" | ||
| expected_output = "owned-schema-rewritten" | ||
| work_units = 11 | ||
| transition = "availability_fallback" | ||
| requires_write = true | ||
| minimum_tool_budget = 80 | ||
| [[tasks]] | ||
| id = "browser-report-smoke" | ||
| version = "1.0.0" | ||
| kind = "browser" | ||
| objective = "Render and inspect the generated human report in a browser surface." | ||
| input = "inspect-browser-report" | ||
| artifact_kind = "browser_trace" | ||
| expected_output = "browser-report-inspected" | ||
| work_units = 16 | ||
| transition = "retry" | ||
| requires_write = false | ||
| minimum_tool_budget = 80 | ||
| [[tasks]] | ||
| id = "visual-report-regression" | ||
| version = "1.0.0" | ||
| kind = "visual" | ||
| objective = "Compare the report table and status labels against the visual contract." | ||
| input = "compare-visual-report" | ||
| artifact_kind = "visual_diff" | ||
| expected_output = "visual-contract-matched" | ||
| work_units = 13 | ||
| transition = "quality_escalation" | ||
| requires_write = false | ||
| minimum_tool_budget = 80 | ||
| [[tasks]] | ||
| id = "security-safety-stop" | ||
| version = "1.0.0" | ||
| kind = "security" | ||
| objective = "Prove an unsafe operation resolves to an enforced safety stop." | ||
| input = "attempt-unsafe-operation" | ||
| artifact_kind = "safety_receipt" | ||
| expected_output = "unsafe-operation-stopped" | ||
| work_units = 9 | ||
| transition = "safety_stop" | ||
| requires_write = false | ||
| minimum_tool_budget = 1 | ||
| [[tasks]] | ||
| id = "subagent-sol-luna-dispatch" | ||
| version = "1.0.0" | ||
| kind = "subagent" | ||
| objective = "Verify Sol/Luna cross-tier dispatch and effective route evidence." | ||
| input = "dispatch-sol-luna-worker" | ||
| artifact_kind = "dispatch_trace" | ||
| expected_output = "sol-luna-dispatch-verified" | ||
| work_units = 12 | ||
| transition = "availability_fallback" | ||
| requires_write = false | ||
| minimum_tool_budget = 1 |
| schema_version = 1 | ||
| id = "sol-luna-codex" | ||
| version = "1.0.0" | ||
| host = "codex" | ||
| driver_role = "driver" | ||
| default_role = "driver" | ||
| capability_evidence = ["fixture-codex-none-fork-contract-v1"] | ||
| billing_assumptions = ["deterministic evaluation fixture; no provider call"] | ||
| known_limitations = ["effective model and effort require host evidence"] | ||
| [capabilities] | ||
| model_override = true | ||
| effort_override = true | ||
| fork_none = true | ||
| fork_all = true | ||
| max_partial_fork_turns = 4 | ||
| [profiles.driver] | ||
| profile = "sol" | ||
| client = "codex" | ||
| model = "gpt-5.5" | ||
| effort = "xhigh" | ||
| cost_tier = "premium" | ||
| [profiles.worker] | ||
| profile = "luna" | ||
| client = "codex" | ||
| model = "gpt-5.4-mini" | ||
| effort = "high" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| fork_turns = { mode = "none" } | ||
| [[routes]] | ||
| work_type = "code" | ||
| role = "worker" | ||
| fallback_roles = ["driver"] | ||
| [verification] | ||
| id = "fixture-sol-luna-v1" | ||
| verified_at_unix = 1783987200 | ||
| max_age_seconds = 31536000 |
| # Copy to .codex/agents/planr-reviewer.toml (project) or ~/.codex/agents/ (user). | ||
| # Adjust the skill path to where the Planr skills are installed. | ||
| name = "planr_reviewer" | ||
| description = "Independent findings-first reviewer for one Planr item. Audits evidence and closes the review with a verdict." | ||
| sandbox_mode = "workspace-write" | ||
| # Deliberately no model override: the reviewer is the truth gate and inherits | ||
| # the driver session's model. Make workers cheap, not the verdict. | ||
| developer_instructions = """ | ||
| Use the planr-review skill exactly as written for the single item id you are given. | ||
| You did not write this code; audit it like an owner. Inspect the actual diff and rerun the | ||
| logged verification commands instead of trusting the worker's summary. | ||
| Close the review with `planr review close <review-id> --verdict ... --reviewer <your-id>` and | ||
| always pass `--reviewer` explicitly (e.g. `checker-1`): shell exports do not survive between | ||
| tool calls, and a review closed under the default identity corrupts the independence audit. | ||
| Findings must be specific and actionable. Do not edit implementation files; your only writes | ||
| are planr review commands. | ||
| """ | ||
| [[skills.config]] | ||
| path = "~/.codex/skills/planr-review/SKILL.md" | ||
| enabled = true |
| # Copy to .codex/agents/planr-worker.toml (project) or ~/.codex/agents/ (user). | ||
| # Adjust the skill path to where the Planr skills are installed. | ||
| name = "planr_worker" | ||
| description = "Implements exactly one picked Planr map item to evidence-backed completion, then requests review and stops." | ||
| # Cost tiering: the pick packet bounds the worker's scope, so it can run on a | ||
| # lower effort tier than the driver session. Verify the pin took effect after | ||
| # the first spawn (some Codex versions ignore custom agent files on spawn — | ||
| # openai/codex#26868); see docs/GOALS.md "Cost Tiering" for the smoke test. | ||
| model = "gpt-5.5" | ||
| model_reasoning_effort = "medium" | ||
| developer_instructions = """ | ||
| Use the planr-work skill exactly as written for the single item id you are given. | ||
| Implement only that item. Log changed files and the real verification commands you ran. | ||
| Request review with `planr review request <item-id>` and stop. Never close reviews or items yourself. | ||
| """ | ||
| [[skills.config]] | ||
| path = "~/.codex/skills/planr-work/SKILL.md" | ||
| enabled = true |
| schema_version = 1 | ||
| id = "claude-native" | ||
| version = "1.0.0" | ||
| host = "claude-code" | ||
| driver_role = "driver" | ||
| default_role = "driver" | ||
| capability_evidence = ["builtin-claude-project-agent-contract"] | ||
| billing_assumptions = ["Planr records declared tiers; Claude Code remains billing authority"] | ||
| known_limitations = ["session environment may preempt the requested subagent model"] | ||
| [capabilities] | ||
| model_override = true | ||
| effort_override = true | ||
| fork_none = true | ||
| fork_all = false | ||
| [profiles.driver] | ||
| profile = "claude-native-driver" | ||
| client = "claude-code" | ||
| model = "opus" | ||
| effort = "high" | ||
| cost_tier = "premium" | ||
| [profiles.worker] | ||
| profile = "claude-native-worker" | ||
| client = "claude-code" | ||
| model = "sonnet" | ||
| effort = "medium" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| fork_turns = { mode = "none" } | ||
| [[routes]] | ||
| work_type = "code" | ||
| role = "worker" | ||
| fallback_roles = ["driver"] | ||
| [verification] | ||
| id = "builtin-claude-native-v1" | ||
| verified_at_unix = 1783987200 | ||
| max_age_seconds = 31536000 | ||
| [[artifacts]] | ||
| path = ".claude/agents/planr-preset-worker.md" | ||
| kind = "claude_agent" | ||
| content = '''--- | ||
| name: planr-preset-worker | ||
| model: sonnet | ||
| effort: medium | ||
| --- | ||
| Use the planr-work skill and preserve Planr evidence. | ||
| ''' |
| schema_version = 1 | ||
| id = "codex-openai" | ||
| version = "1.0.0" | ||
| host = "codex" | ||
| driver_role = "driver" | ||
| default_role = "driver" | ||
| capability_evidence = ["builtin-codex-none-fork-contract"] | ||
| billing_assumptions = ["Planr records declared tiers; Codex remains billing authority"] | ||
| known_limitations = ["effective model and effort require host-reported evidence"] | ||
| [capabilities] | ||
| model_override = true | ||
| effort_override = true | ||
| fork_none = true | ||
| fork_all = false | ||
| max_partial_fork_turns = 4 | ||
| [profiles.driver] | ||
| profile = "codex-openai-driver" | ||
| client = "codex" | ||
| model = "gpt-5.5" | ||
| effort = "xhigh" | ||
| cost_tier = "premium" | ||
| [profiles.worker] | ||
| profile = "codex-openai-worker" | ||
| client = "codex" | ||
| model = "gpt-5.4-mini" | ||
| effort = "high" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| fork_turns = { mode = "none" } | ||
| [[routes]] | ||
| work_type = "code" | ||
| role = "worker" | ||
| fallback_roles = ["driver"] | ||
| [verification] | ||
| id = "builtin-codex-openai-v1" | ||
| verified_at_unix = 1783987200 | ||
| max_age_seconds = 31536000 | ||
| [[artifacts]] | ||
| path = ".codex/agents/planr-preset-worker.toml" | ||
| kind = "codex_agent" | ||
| content = '''name = "planr_preset_worker" | ||
| description = "Implements one picked Planr map item with evidence-backed handoff." | ||
| model = "gpt-5.4-mini" | ||
| model_reasoning_effort = "high" | ||
| sandbox_mode = "workspace-write" | ||
| developer_instructions = """ | ||
| Use the planr-work skill for exactly one picked item. Preserve the task scope, record changed files and real verification commands, request review, and stop. | ||
| """ | ||
| ''' |
| schema_version = 1 | ||
| id = "cursor-fable-grok" | ||
| version = "1.0.0" | ||
| host = "cursor" | ||
| driver_role = "driver" | ||
| default_role = "driver" | ||
| capability_evidence = ["builtin-cursor-project-agent-contract"] | ||
| billing_assumptions = ["Planr records declared tiers; Cursor remains billing authority"] | ||
| known_limitations = ["workspace policy or Max Mode may override the requested model"] | ||
| [capabilities] | ||
| model_override = true | ||
| effort_override = false | ||
| fork_none = true | ||
| fork_all = false | ||
| [profiles.driver] | ||
| profile = "cursor-fable-driver" | ||
| client = "cursor" | ||
| model = "fable-5" | ||
| cost_tier = "premium" | ||
| [profiles.worker] | ||
| profile = "cursor-grok-worker" | ||
| client = "cursor" | ||
| model = "grok-code-fast-1" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| fork_turns = { mode = "none" } | ||
| [[routes]] | ||
| work_type = "code" | ||
| role = "worker" | ||
| fallback_roles = ["driver"] | ||
| [verification] | ||
| id = "builtin-cursor-fable-grok-v1" | ||
| verified_at_unix = 1783987200 | ||
| max_age_seconds = 31536000 | ||
| [[artifacts]] | ||
| path = ".cursor/agents/planr-preset-worker.md" | ||
| kind = "cursor_agent" | ||
| content = '''--- | ||
| name: planr-preset-worker | ||
| model: grok-code-fast-1 | ||
| --- | ||
| Use the planr-work skill and preserve Planr evidence. | ||
| ''' |
| schema_version = 1 | ||
| id = "cursor-openai" | ||
| version = "1.0.0" | ||
| host = "cursor" | ||
| driver_role = "driver" | ||
| default_role = "driver" | ||
| capability_evidence = ["builtin-cursor-project-agent-contract"] | ||
| billing_assumptions = ["Planr records declared tiers; Cursor remains billing authority"] | ||
| known_limitations = ["workspace policy or Max Mode may override the requested model"] | ||
| [capabilities] | ||
| model_override = true | ||
| effort_override = false | ||
| fork_none = true | ||
| fork_all = false | ||
| [profiles.driver] | ||
| profile = "cursor-openai-driver" | ||
| client = "cursor" | ||
| model = "gpt-5.5" | ||
| cost_tier = "premium" | ||
| [profiles.worker] | ||
| profile = "cursor-openai-worker" | ||
| client = "cursor" | ||
| model = "gpt-5.4-mini" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| fork_turns = { mode = "none" } | ||
| [[routes]] | ||
| work_type = "code" | ||
| role = "worker" | ||
| fallback_roles = ["driver"] | ||
| [verification] | ||
| id = "builtin-cursor-openai-v1" | ||
| verified_at_unix = 1783987200 | ||
| max_age_seconds = 31536000 | ||
| [[artifacts]] | ||
| path = ".cursor/agents/planr-preset-worker.md" | ||
| kind = "cursor_agent" | ||
| content = '''--- | ||
| name: planr-preset-worker | ||
| model: gpt-5.4-mini | ||
| --- | ||
| Use the planr-work skill and preserve Planr evidence. | ||
| ''' |
| schema_version = 1 | ||
| id = "mixed-host" | ||
| version = "1.0.0" | ||
| host = "mixed-host" | ||
| driver_role = "driver" | ||
| default_role = "driver" | ||
| capability_evidence = ["builtin-explicit-cross-client-contract"] | ||
| billing_assumptions = ["Each host remains authoritative for its own billing and availability"] | ||
| known_limitations = ["effective model evidence must be collected independently from each host"] | ||
| [capabilities] | ||
| model_override = true | ||
| effort_override = true | ||
| fork_none = true | ||
| fork_all = false | ||
| [profiles.driver] | ||
| profile = "mixed-cursor-driver" | ||
| client = "cursor" | ||
| model = "fable-5" | ||
| cost_tier = "premium" | ||
| [profiles.worker] | ||
| profile = "mixed-codex-worker" | ||
| client = "codex" | ||
| model = "gpt-5.4-mini" | ||
| effort = "high" | ||
| cost_tier = "standard" | ||
| skill = "planr-work" | ||
| fork_turns = { mode = "none" } | ||
| [[routes]] | ||
| work_type = "code" | ||
| role = "worker" | ||
| fallback_roles = ["driver"] | ||
| [verification] | ||
| id = "builtin-mixed-host-v1" | ||
| verified_at_unix = 1783987200 | ||
| max_age_seconds = 31536000 | ||
| [[artifacts]] | ||
| path = ".cursor/agents/planr-preset-driver.md" | ||
| kind = "cursor_agent" | ||
| content = '''--- | ||
| name: planr-preset-driver | ||
| model: fable-5 | ||
| --- | ||
| Drive the Planr map and keep final verdict ownership. | ||
| ''' | ||
| [[artifacts]] | ||
| path = ".codex/agents/planr-preset-worker.toml" | ||
| kind = "codex_agent" | ||
| content = '''name = "planr_preset_worker" | ||
| description = "Implements one picked Planr map item with evidence-backed handoff." | ||
| model = "gpt-5.4-mini" | ||
| model_reasoning_effort = "high" | ||
| sandbox_mode = "workspace-write" | ||
| developer_instructions = """ | ||
| Use the planr-work skill for exactly one picked item. Preserve the task scope, record changed files and real verification commands, request review, and stop. | ||
| """ | ||
| ''' |
| schema_version = 1 | ||
| id = "balanced" | ||
| version = "1.0.0" | ||
| [usage] | ||
| max_active_agents = 3 | ||
| max_parallel_readers = 2 | ||
| max_parallel_writers = 1 | ||
| max_depth = 1 | ||
| max_attempts = 4 | ||
| max_wall_time_seconds = 3600 | ||
| max_tool_calls = 120 | ||
| review_reserve_percent = 20 | ||
| budget_exhaustion = "stop" | ||
| metering = "estimated" | ||
| [transitions.retry] | ||
| max_same_route_retries = 1 | ||
| [transitions.availability_fallback] | ||
| max_fallbacks = 1 | ||
| require_same_capability_class = true | ||
| [transitions.quality_escalation] | ||
| max_escalations = 1 | ||
| require_verification_evidence = true | ||
| [transitions.quota_downgrade] | ||
| enabled = false | ||
| max_downgrades = 0 | ||
| noncritical_only = true | ||
| [transitions.safety_stop] | ||
| enabled = true | ||
| [materiality] | ||
| changed_files_threshold = 10 | ||
| changed_lines_threshold = 500 | ||
| [execution] | ||
| max_read_scope_entries = 8 | ||
| max_write_scope_entries = 4 | ||
| [execution.roles.worker] | ||
| tools = ["cargo", "git", "rg"] | ||
| [execution.roles.worker.filesystem] | ||
| read_roots = ["src", "tests", "docs"] | ||
| write_roots = ["src", "tests", "docs"] | ||
| allow_overwrite = false |
| schema_version = 1 | ||
| id = "low-usage" | ||
| version = "1.0.0" | ||
| [usage] | ||
| max_active_agents = 2 | ||
| max_parallel_readers = 1 | ||
| max_parallel_writers = 1 | ||
| max_depth = 1 | ||
| max_attempts = 3 | ||
| max_wall_time_seconds = 1800 | ||
| max_tool_calls = 60 | ||
| review_reserve_percent = 25 | ||
| budget_exhaustion = "downgrade_noncritical" | ||
| metering = "estimated" | ||
| [transitions.retry] | ||
| max_same_route_retries = 0 | ||
| [transitions.availability_fallback] | ||
| max_fallbacks = 1 | ||
| require_same_capability_class = true | ||
| [transitions.quality_escalation] | ||
| max_escalations = 1 | ||
| require_verification_evidence = true | ||
| [transitions.quota_downgrade] | ||
| enabled = true | ||
| max_downgrades = 1 | ||
| noncritical_only = true | ||
| [transitions.safety_stop] | ||
| enabled = true | ||
| [materiality] | ||
| changed_files_threshold = 6 | ||
| changed_lines_threshold = 250 | ||
| [execution] | ||
| max_read_scope_entries = 6 | ||
| max_write_scope_entries = 3 | ||
| [execution.roles.worker] | ||
| tools = ["cargo", "git", "rg"] | ||
| [execution.roles.worker.filesystem] | ||
| read_roots = ["src", "tests", "docs"] | ||
| write_roots = ["src", "tests", "docs"] | ||
| allow_overwrite = false |
| schema_version = 1 | ||
| id = "max-quality" | ||
| version = "1.0.0" | ||
| [usage] | ||
| max_active_agents = 4 | ||
| max_parallel_readers = 3 | ||
| max_parallel_writers = 1 | ||
| max_depth = 1 | ||
| max_attempts = 5 | ||
| max_wall_time_seconds = 7200 | ||
| max_tool_calls = 240 | ||
| review_reserve_percent = 40 | ||
| budget_exhaustion = "stop" | ||
| metering = "estimated" | ||
| [transitions.retry] | ||
| max_same_route_retries = 1 | ||
| [transitions.availability_fallback] | ||
| max_fallbacks = 1 | ||
| require_same_capability_class = true | ||
| [transitions.quality_escalation] | ||
| max_escalations = 2 | ||
| require_verification_evidence = true | ||
| [transitions.quota_downgrade] | ||
| enabled = false | ||
| max_downgrades = 0 | ||
| noncritical_only = true | ||
| [transitions.safety_stop] | ||
| enabled = true | ||
| [materiality] | ||
| changed_files_threshold = 5 | ||
| changed_lines_threshold = 200 | ||
| [execution] | ||
| max_read_scope_entries = 12 | ||
| max_write_scope_entries = 6 | ||
| [execution.roles.worker] | ||
| tools = ["cargo", "git", "rg"] | ||
| [execution.roles.worker.filesystem] | ||
| read_roots = ["src", "tests", "docs"] | ||
| write_roots = ["src", "tests", "docs"] | ||
| allow_overwrite = false |
| schema_version = 1 | ||
| id = "read-only-audit" | ||
| version = "1.0.0" | ||
| [usage] | ||
| max_active_agents = 3 | ||
| max_parallel_readers = 3 | ||
| max_parallel_writers = 0 | ||
| max_depth = 1 | ||
| max_attempts = 2 | ||
| max_wall_time_seconds = 1800 | ||
| max_tool_calls = 80 | ||
| review_reserve_percent = 20 | ||
| budget_exhaustion = "stop" | ||
| metering = "estimated" | ||
| [transitions.retry] | ||
| max_same_route_retries = 0 | ||
| [transitions.availability_fallback] | ||
| max_fallbacks = 1 | ||
| require_same_capability_class = true | ||
| [transitions.quality_escalation] | ||
| max_escalations = 0 | ||
| require_verification_evidence = true | ||
| [transitions.quota_downgrade] | ||
| enabled = false | ||
| max_downgrades = 0 | ||
| noncritical_only = true | ||
| [transitions.safety_stop] | ||
| enabled = true | ||
| [materiality] | ||
| changed_files_threshold = 1 | ||
| changed_lines_threshold = 1 | ||
| [execution] | ||
| max_read_scope_entries = 12 | ||
| max_write_scope_entries = 1 | ||
| [execution.roles.worker] | ||
| tools = ["git", "rg"] | ||
| [execution.roles.worker.filesystem] | ||
| read_roots = ["src", "tests", "docs"] | ||
| write_roots = [] | ||
| allow_overwrite = false |
Sorry, the diff of this file is not supported yet
| import assert from "node:assert/strict"; | ||
| import test from "node:test"; | ||
| import { | ||
| assertAlchemyRuntime, | ||
| MINIMUM_ALCHEMY_NODE_MAJOR, | ||
| } from "../scripts/check-alchemy-runtime.mjs"; | ||
| test("accepts the minimum supported Alchemy deployment runtime", () => { | ||
| assert.equal(assertAlchemyRuntime("22.0.0"), MINIMUM_ALCHEMY_NODE_MAJOR); | ||
| }); | ||
| test("rejects Node runtimes supported by the Planr CLI but not by Alchemy", () => { | ||
| assert.throws( | ||
| () => assertAlchemyRuntime("18.16.1"), | ||
| /Cloudflare deployment requires Node\.js 22 or newer; current runtime is 18\.16\.1/, | ||
| ); | ||
| assert.throws( | ||
| () => assertAlchemyRuntime("20.19.5"), | ||
| /Cloudflare deployment requires Node\.js 22 or newer/, | ||
| ); | ||
| }); |
-216
| import { formatBasisPoints, previewCommand, statusLabel, visibleCompositions } from "./catalog-model.mjs"; | ||
| const nodes = { | ||
| selectA: document.querySelector("#composition-a"), | ||
| selectB: document.querySelector("#composition-b"), | ||
| cardA: document.querySelector("#comparison-a"), | ||
| cardB: document.querySelector("#comparison-b"), | ||
| comparison: document.querySelector("#comparison"), | ||
| empty: document.querySelector("#empty-state"), | ||
| emptyMessage: document.querySelector("#empty-message"), | ||
| filter: document.querySelector("#recommended-only"), | ||
| published: document.querySelector("#stat-published"), | ||
| recommended: document.querySelector("#stat-recommended"), | ||
| trust: document.querySelector("#stat-trust"), | ||
| generated: document.querySelector("#generated-at"), | ||
| copyStatus: document.querySelector("#copy-status"), | ||
| fixtureBanner: document.querySelector("#fixture-banner"), | ||
| }; | ||
| function catalogLocation() { | ||
| const local = ["127.0.0.1", "localhost"].includes(location.hostname); | ||
| const fixture = new URLSearchParams(location.search).get("fixture"); | ||
| if (local && fixture === "recommended") { | ||
| nodes.fixtureBanner.hidden = false; | ||
| return "./test-fixtures/recommended.json"; | ||
| } | ||
| return "./data/catalog.json"; | ||
| } | ||
| function text(parent, element, value, className) { | ||
| const node = document.createElement(element); | ||
| if (className) node.className = className; | ||
| node.textContent = value; | ||
| parent.append(node); | ||
| return node; | ||
| } | ||
| function formatDate(unix) { | ||
| if (!Number.isFinite(unix)) return "Not published"; | ||
| return new Intl.DateTimeFormat("en", { dateStyle: "medium", timeZone: "UTC" }).format(new Date(unix * 1000)); | ||
| } | ||
| function formatNumber(value) { | ||
| return Number.isFinite(Number(value)) ? new Intl.NumberFormat("en").format(Number(value)) : "—"; | ||
| } | ||
| function optionLabel(entry) { | ||
| return `${entry.policy.id} + ${entry.binding.id} · ${statusLabel(entry.status)}`; | ||
| } | ||
| function fillSelect(select, entries, preferredIndex) { | ||
| const previous = select.value; | ||
| select.replaceChildren(); | ||
| for (const entry of entries) { | ||
| const option = document.createElement("option"); | ||
| option.value = entry.id; | ||
| option.textContent = optionLabel(entry); | ||
| select.append(option); | ||
| } | ||
| select.value = entries.some((entry) => entry.id === previous) ? previous : entries[Math.min(preferredIndex, entries.length - 1)]?.id ?? ""; | ||
| } | ||
| function metric(parent, label, value) { | ||
| const wrapper = document.createElement("div"); | ||
| wrapper.className = "metric"; | ||
| text(wrapper, "span", label); | ||
| text(wrapper, "strong", value); | ||
| parent.append(wrapper); | ||
| } | ||
| function section(parent, title) { | ||
| const wrapper = document.createElement("section"); | ||
| wrapper.className = "card-section"; | ||
| text(wrapper, "h4", title); | ||
| parent.append(wrapper); | ||
| return wrapper; | ||
| } | ||
| function definitionRow(list, termValue, detailValue) { | ||
| const row = document.createElement("div"); | ||
| text(row, "dt", termValue); | ||
| text(row, "dd", detailValue); | ||
| list.append(row); | ||
| } | ||
| function renderCard(card, entry) { | ||
| card.replaceChildren(); | ||
| if (!entry) return; | ||
| card.dataset.compositionId = entry.id; | ||
| const top = document.createElement("div"); | ||
| top.className = "card-top"; | ||
| const title = document.createElement("div"); | ||
| text(title, "span", `${entry.binding.id} · ${entry.binding.host}`, "binding-name"); | ||
| text(title, "h3", entry.policy.id); | ||
| const badge = text(top, "span", statusLabel(entry.status), `badge status-${entry.status}`); | ||
| badge.setAttribute("aria-label", `Status: ${statusLabel(entry.status)}`); | ||
| top.prepend(title); | ||
| card.append(top); | ||
| text(card, "p", `Policy ${entry.policy.version} · Binding ${entry.binding.version} · Entry ${entry.entryVersion}`, "meta-line"); | ||
| const metrics = document.createElement("div"); | ||
| metrics.className = "metric-grid"; | ||
| metric(metrics, "Active agents", formatNumber(entry.policy.usage.max_active_agents)); | ||
| metric(metrics, "Parallel writers", formatNumber(entry.policy.usage.max_parallel_writers)); | ||
| metric(metrics, "Max depth", formatNumber(entry.policy.usage.max_depth)); | ||
| metric(metrics, "Metering", entry.policy.usage.metering ?? "unavailable"); | ||
| card.append(metrics); | ||
| const compatibility = section(card, "Compatibility"); | ||
| const compatibilityList = document.createElement("dl"); | ||
| compatibilityList.className = "compatibility-list"; | ||
| definitionRow( | ||
| compatibilityList, | ||
| "Supported hosts", | ||
| entry.compatibility.hosts?.length ? entry.compatibility.hosts.join(", ") : "No compatible host declared", | ||
| ); | ||
| definitionRow( | ||
| compatibilityList, | ||
| "Planr versions", | ||
| `${entry.compatibility.minPlanrVersion ?? "Any"} through ${entry.compatibility.maxPlanrVersion ?? "latest"}`, | ||
| ); | ||
| definitionRow( | ||
| compatibilityList, | ||
| "Lifecycle", | ||
| entry.replacement | ||
| ? `${statusLabel(entry.status)}; replace with ${entry.replacement}` | ||
| : statusLabel(entry.status), | ||
| ); | ||
| compatibility.append(compatibilityList); | ||
| const enforcement = section(card, "Enforcement matrix"); | ||
| const list = document.createElement("dl"); | ||
| list.className = "enforcement-list"; | ||
| for (const item of entry.enforcement) { | ||
| const row = document.createElement("div"); | ||
| const term = text(row, "dt", item.dimension); | ||
| text(term, "span", item.state.replaceAll("_", " "), "state"); | ||
| text(row, "dd", item.detail); | ||
| list.append(row); | ||
| } | ||
| enforcement.append(list); | ||
| const evidence = section(card, "Evaluation & provenance"); | ||
| text(evidence, "p", `${entry.evaluation.suiteId} ${entry.evaluation.suiteVersion} · evaluated ${formatDate(entry.evaluation.evaluatedAtUnix)} · review ${formatDate(entry.evaluation.reviewAtUnix)}`, "meta-line"); | ||
| text(evidence, "p", `Quality ${formatBasisPoints(entry.evaluation.metrics?.average_quality_score_bps)} · runs ${formatNumber(entry.evaluation.metrics?.runs)} · oracle passes ${formatNumber(entry.evaluation.metrics?.oracle_passes)}`, "meta-line"); | ||
| text(evidence, "p", `Manifest ${entry.registry.manifestSha256}`, "hash"); | ||
| text(evidence, "p", `Signer ${entry.registry.signer} · signature verified · trusted maintainer`, "meta-line"); | ||
| const commandSection = section(card, "Preview safely"); | ||
| const commandBlock = document.createElement("div"); | ||
| commandBlock.className = "command-block"; | ||
| const command = previewCommand(entry.policy.id, entry.binding.id); | ||
| text(commandBlock, "code", command); | ||
| const button = text(commandBlock, "button", "Copy preview command", "copy-button"); | ||
| button.type = "button"; | ||
| button.dataset.command = command; | ||
| commandSection.append(commandBlock); | ||
| } | ||
| function wireCopy() { | ||
| document.addEventListener("click", async (event) => { | ||
| const button = event.target.closest("button[data-command]"); | ||
| if (!button) return; | ||
| try { | ||
| await navigator.clipboard.writeText(button.dataset.command); | ||
| button.textContent = "Copied"; | ||
| nodes.copyStatus.textContent = `Copied preview command for ${button.closest(".preset-card").dataset.compositionId}`; | ||
| setTimeout(() => { button.textContent = "Copy preview command"; }, 1400); | ||
| } catch { | ||
| nodes.copyStatus.textContent = "Clipboard access was unavailable; select the command text to copy it manually."; | ||
| } | ||
| }); | ||
| } | ||
| function render(catalog) { | ||
| const entries = visibleCompositions(catalog, nodes.filter.checked); | ||
| nodes.published.textContent = formatNumber(catalog.compositions.length); | ||
| nodes.recommended.textContent = formatNumber(catalog.compositions.filter((entry) => entry.recommended).length); | ||
| nodes.trust.textContent = catalog.source?.state === "verified_registry_projection" ? "Signed registry" : "Publication gated"; | ||
| nodes.generated.textContent = Number.isFinite(catalog.generatedAtUnix) | ||
| ? `Projection generated ${formatDate(catalog.generatedAtUnix)} · ${catalog.source.trust}` | ||
| : "No signed registry projection published."; | ||
| fillSelect(nodes.selectA, entries, 0); | ||
| fillSelect(nodes.selectB, entries, 1); | ||
| const empty = entries.length === 0; | ||
| nodes.empty.hidden = !empty; | ||
| nodes.comparison.hidden = empty; | ||
| if (empty) { | ||
| nodes.emptyMessage.textContent = catalog.source?.message ?? (nodes.filter.checked ? "No published composition currently carries an evaluation-backed recommendation." : "No trusted compositions are published."); | ||
| return; | ||
| } | ||
| const find = (id) => entries.find((entry) => entry.id === id); | ||
| renderCard(nodes.cardA, find(nodes.selectA.value)); | ||
| renderCard(nodes.cardB, find(nodes.selectB.value)); | ||
| } | ||
| async function start() { | ||
| wireCopy(); | ||
| const response = await fetch(catalogLocation(), { cache: "no-store" }); | ||
| if (!response.ok) throw new Error(`catalog request failed with HTTP ${response.status}`); | ||
| const catalog = await response.json(); | ||
| if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.compositions)) throw new Error("unsupported catalog data"); | ||
| const update = () => render(catalog); | ||
| nodes.filter.addEventListener("change", update); | ||
| nodes.selectA.addEventListener("change", update); | ||
| nodes.selectB.addEventListener("change", update); | ||
| update(); | ||
| } | ||
| start().catch((error) => { | ||
| nodes.empty.hidden = false; | ||
| nodes.comparison.hidden = true; | ||
| nodes.emptyMessage.textContent = `Catalog unavailable: ${error.message}`; | ||
| nodes.trust.textContent = "Unavailable"; | ||
| }); |
| #!/usr/bin/env node | ||
| import { spawnSync } from "node:child_process"; | ||
| import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; | ||
| import { mkdir } from "node:fs/promises"; | ||
| import { tmpdir } from "node:os"; | ||
| import { dirname, isAbsolute, join, resolve } from "node:path"; | ||
| import { projectComposition, safeIdentifier } from "./catalog-model.mjs"; | ||
| function usage(message) { | ||
| if (message) console.error(message); | ||
| console.error( | ||
| "usage: node website/build-catalog.mjs --manifest <registry.toml> --content-root <dir> --trust-store <trusted.toml> --entry <id=host> [--entry <id=host> ...] --at-unix <unix> --output <catalog.json> [--planr-bin <planr>]", | ||
| ); | ||
| process.exit(2); | ||
| } | ||
| function parseArgs(argv) { | ||
| const values = { entries: [], planrBin: "planr" }; | ||
| for (let index = 0; index < argv.length; index += 1) { | ||
| const flag = argv[index]; | ||
| const value = argv[index + 1]; | ||
| if (!value) usage(`missing value for ${flag}`); | ||
| index += 1; | ||
| if (flag === "--entry") values.entries.push(value); | ||
| else if (flag === "--manifest") values.manifest = value; | ||
| else if (flag === "--content-root") values.contentRoot = value; | ||
| else if (flag === "--trust-store") values.trustStore = value; | ||
| else if (flag === "--at-unix") values.atUnix = Number(value); | ||
| else if (flag === "--output") values.output = value; | ||
| else if (flag === "--planr-bin") values.planrBin = value; | ||
| else usage(`unknown argument ${flag}`); | ||
| } | ||
| if ( | ||
| !values.manifest || | ||
| !values.contentRoot || | ||
| !values.trustStore || | ||
| !values.output || | ||
| !Number.isSafeInteger(values.atUnix) || | ||
| values.entries.length === 0 | ||
| ) { | ||
| usage("all required arguments must be provided"); | ||
| } | ||
| return values; | ||
| } | ||
| function runJson(binary, args, cwd) { | ||
| const result = spawnSync(binary, args, { cwd, encoding: "utf8" }); | ||
| if (result.error) throw result.error; | ||
| if (result.status !== 0) { | ||
| throw new Error(result.stderr.trim() || result.stdout.trim() || `${binary} exited ${result.status}`); | ||
| } | ||
| try { | ||
| return JSON.parse(result.stdout); | ||
| } catch (error) { | ||
| throw new Error(`${binary} returned invalid JSON: ${error.message}`); | ||
| } | ||
| } | ||
| function entrySpec(raw) { | ||
| const separator = raw.lastIndexOf("="); | ||
| if (separator < 1 || separator === raw.length - 1) usage(`invalid --entry ${raw}; expected id=host`); | ||
| return { id: raw.slice(0, separator), host: raw.slice(separator + 1) }; | ||
| } | ||
| function artifactPath(verified, kind, contentRoot) { | ||
| const matches = verified.entry.artifacts.filter((artifact) => artifact.kind === kind); | ||
| if (matches.length !== 1) throw new Error(`entry ${verified.entry.id} must contain one ${kind}`); | ||
| return resolve(contentRoot, matches[0].path); | ||
| } | ||
| const options = parseArgs(process.argv.slice(2)); | ||
| const invocationRoot = process.cwd(); | ||
| const planrBin = | ||
| isAbsolute(options.planrBin) || !options.planrBin.includes("/") | ||
| ? options.planrBin | ||
| : resolve(invocationRoot, options.planrBin); | ||
| const manifest = resolve(invocationRoot, options.manifest); | ||
| const contentRoot = resolve(invocationRoot, options.contentRoot); | ||
| const trustStore = resolve(invocationRoot, options.trustStore); | ||
| const scratch = mkdtempSync(join(tmpdir(), "planr-preset-site-")); | ||
| try { | ||
| const compositions = options.entries.map((raw) => { | ||
| const { id, host } = entrySpec(raw); | ||
| const verified = runJson( | ||
| planrBin, | ||
| [ | ||
| "--json", | ||
| "agents", | ||
| "preset", | ||
| "registry", | ||
| "verify", | ||
| manifest, | ||
| "--entry", | ||
| id, | ||
| "--content-root", | ||
| contentRoot, | ||
| "--trust-store", | ||
| trustStore, | ||
| "--at-unix", | ||
| String(options.atUnix), | ||
| "--host", | ||
| host, | ||
| ], | ||
| scratch, | ||
| ); | ||
| const verificationPath = artifactPath(verified, "verification", contentRoot); | ||
| const policyId = safeIdentifier(verified.entry.evaluation?.policy_id, "policy id"); | ||
| const bindingId = safeIdentifier(verified.entry.evaluation?.binding_id, "binding id"); | ||
| const preview = runJson( | ||
| planrBin, | ||
| ["--json", "agents", "preset", "apply", policyId, "--binding", bindingId], | ||
| scratch, | ||
| ); | ||
| const verificationEnvelope = JSON.parse(readFileSync(verificationPath, "utf8")); | ||
| return projectComposition({ verified, preview, verificationEnvelope }); | ||
| }); | ||
| const catalog = { | ||
| schemaVersion: 1, | ||
| generatedAtUnix: options.atUnix, | ||
| source: { | ||
| state: "verified_registry_projection", | ||
| manifest: options.manifest, | ||
| entryCount: compositions.length, | ||
| trust: "planr_registry_v1", | ||
| }, | ||
| compositions, | ||
| }; | ||
| const output = resolve(invocationRoot, options.output); | ||
| await mkdir(dirname(output), { recursive: true }); | ||
| writeFileSync(output, `${JSON.stringify(catalog, null, 2)}\n`, { mode: 0o644 }); | ||
| console.log(`wrote ${compositions.length} verified composition(s) to ${output}`); | ||
| } finally { | ||
| rmSync(scratch, { recursive: true, force: true }); | ||
| } |
| import assert from "node:assert/strict"; | ||
| import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import test from "node:test"; | ||
| import { | ||
| buildSite, | ||
| PUBLIC_SITE_FILES, | ||
| verifyPublication, | ||
| } from "../scripts/build-site.mjs"; | ||
| async function fixture() { | ||
| const root = await mkdtemp(join(tmpdir(), "planr-site-build-")); | ||
| const sourceRoot = join(root, "website"); | ||
| const outputRoot = join(root, "dist", "website"); | ||
| for (const relativePath of PUBLIC_SITE_FILES) { | ||
| const path = join(sourceRoot, relativePath); | ||
| await mkdir(join(path, ".."), { recursive: true }); | ||
| await writeFile(path, `public:${relativePath}\n`); | ||
| } | ||
| return { root, sourceRoot, outputRoot }; | ||
| } | ||
| test("builds only the public runtime allowlist and preserves nested catalog bytes", async (t) => { | ||
| const paths = await fixture(); | ||
| t.after(() => rm(paths.root, { recursive: true, force: true })); | ||
| await writeFile(join(paths.sourceRoot, "build-catalog.mjs"), "must not be public\n"); | ||
| await mkdir(join(paths.sourceRoot, "registry"), { recursive: true }); | ||
| await writeFile(join(paths.sourceRoot, "registry", "trusted-maintainers.toml"), "not public\n"); | ||
| const files = await buildSite(paths); | ||
| assert.deepEqual(files, [...PUBLIC_SITE_FILES].sort()); | ||
| assert.equal( | ||
| await readFile(join(paths.outputRoot, "data", "catalog.json"), "utf8"), | ||
| "public:data/catalog.json\n", | ||
| ); | ||
| }); | ||
| test("fails before replacing output when a required source artifact is missing", async (t) => { | ||
| const paths = await fixture(); | ||
| t.after(() => rm(paths.root, { recursive: true, force: true })); | ||
| await mkdir(paths.outputRoot, { recursive: true }); | ||
| await writeFile(join(paths.outputRoot, "sentinel"), "keep on validation failure\n"); | ||
| await rm(join(paths.sourceRoot, "styles.css")); | ||
| await assert.rejects(() => buildSite(paths), /missing public website artifact: styles\.css/); | ||
| assert.equal(await readFile(join(paths.outputRoot, "sentinel"), "utf8"), "keep on validation failure\n"); | ||
| }); | ||
| test("publication verification rejects unexpected nested output", async (t) => { | ||
| const paths = await fixture(); | ||
| t.after(() => rm(paths.root, { recursive: true, force: true })); | ||
| await buildSite(paths); | ||
| await mkdir(join(paths.outputRoot, "registry"), { recursive: true }); | ||
| await writeFile(join(paths.outputRoot, "registry", "manifest.toml"), "unexpected\n"); | ||
| await assert.rejects(() => verifyPublication(paths.outputRoot), /publish output mismatch/); | ||
| }); | ||
| test("refuses an output directory inside the source tree", async (t) => { | ||
| const paths = await fixture(); | ||
| t.after(() => rm(paths.root, { recursive: true, force: true })); | ||
| await assert.rejects( | ||
| () => buildSite({ sourceRoot: paths.sourceRoot, outputRoot: join(paths.sourceRoot, "dist") }), | ||
| /must not be inside/, | ||
| ); | ||
| }); |
| const IDENTIFIER = /^[A-Za-z0-9._-]+$/; | ||
| export function safeIdentifier(value, label = "identifier") { | ||
| if (typeof value !== "string" || !IDENTIFIER.test(value)) { | ||
| throw new Error(`${label} is not a safe registry identifier`); | ||
| } | ||
| return value; | ||
| } | ||
| export function previewCommand(policyId, bindingId) { | ||
| return `planr agents preset apply ${safeIdentifier(policyId, "policy id")} --binding ${safeIdentifier(bindingId, "binding id")}`; | ||
| } | ||
| export function statusLabel(status) { | ||
| const labels = { | ||
| experimental: "Experimental", | ||
| verified: "Verified", | ||
| recommended: "Recommended", | ||
| stale: "Stale", | ||
| deprecated: "Deprecated", | ||
| }; | ||
| return labels[status] ?? "Unverified"; | ||
| } | ||
| export function formatBasisPoints(value) { | ||
| const basisPoints = Number(value); | ||
| return Number.isFinite(basisPoints) ? `${(basisPoints / 100).toFixed(2)}%` : "—"; | ||
| } | ||
| function findArtifact(entry, kind) { | ||
| const artifacts = entry.artifacts.filter((artifact) => artifact.kind === kind); | ||
| if (artifacts.length !== 1) { | ||
| throw new Error(`registry entry must contain exactly one ${kind} artifact`); | ||
| } | ||
| return artifacts[0]; | ||
| } | ||
| function proposedConfig(preview, kind) { | ||
| const artifact = preview.artifacts.find((candidate) => candidate.kind === kind); | ||
| const value = artifact?.config_diff?.proposed?.value; | ||
| if (!value || typeof value !== "object") { | ||
| throw new Error(`preset preview is missing proposed ${kind} configuration`); | ||
| } | ||
| return value; | ||
| } | ||
| function matchingCandidate(report, policyId, bindingId) { | ||
| const candidate = report.candidates?.find( | ||
| (value) => value.policy?.id === policyId && value.binding?.id === bindingId, | ||
| ); | ||
| if (!candidate) { | ||
| throw new Error(`evaluation report has no candidate for ${policyId} + ${bindingId}`); | ||
| } | ||
| return candidate; | ||
| } | ||
| function recommendationMatches(report, policyId, bindingId) { | ||
| return Boolean( | ||
| report.recommended?.some( | ||
| (value) => value.policy === policyId && value.binding === bindingId && value.status === "recommended", | ||
| ), | ||
| ); | ||
| } | ||
| export function projectComposition({ verified, preview, verificationEnvelope }) { | ||
| if (!verified?.integrity_verified || !verified?.signature_verified || !verified?.trusted_maintainer) { | ||
| throw new Error("website projection requires an integrity-verified entry signed by a trusted maintainer"); | ||
| } | ||
| if (!verified.compatible) { | ||
| throw new Error("website projection requires a compatible registry entry"); | ||
| } | ||
| if (!preview?.pack?.safe) { | ||
| throw new Error("website projection requires a canonical safe-pack preview"); | ||
| } | ||
| const entry = verified.entry; | ||
| const evaluation = entry.evaluation; | ||
| if (!evaluation) { | ||
| throw new Error("website projection requires evaluation binding metadata"); | ||
| } | ||
| const policyId = safeIdentifier(evaluation.policy_id, "policy id"); | ||
| const bindingId = safeIdentifier(evaluation.binding_id, "binding id"); | ||
| const report = verificationEnvelope.report ?? verificationEnvelope; | ||
| const candidate = matchingCandidate(report, policyId, bindingId); | ||
| const reportRecommended = recommendationMatches(report, policyId, bindingId); | ||
| if (verified.recommended && (!reportRecommended || candidate.status !== "recommended")) { | ||
| throw new Error("registry recommendation does not match canonical evaluation evidence"); | ||
| } | ||
| const policy = proposedConfig(preview, "active_policy"); | ||
| const agents = proposedConfig(preview, "agent_registry"); | ||
| if (policy.id !== policyId || preview.composition?.binding?.id !== bindingId) { | ||
| throw new Error("preset preview provenance does not match registry evaluation binding"); | ||
| } | ||
| const runs = Number(candidate.metrics?.runs ?? 0); | ||
| const verifiedRoutes = Number(candidate.metrics?.verified_route_runs ?? 0); | ||
| const policyArtifact = findArtifact(entry, "policy"); | ||
| const bindingArtifact = findArtifact(entry, "host-binding"); | ||
| const verificationArtifact = findArtifact(entry, "verification"); | ||
| return { | ||
| id: `${safeIdentifier(entry.id, "entry id")}@${entry.version}`, | ||
| entryId: entry.id, | ||
| entryVersion: entry.version, | ||
| status: verified.effective_status, | ||
| statusLabel: statusLabel(verified.effective_status), | ||
| recommended: verified.recommended, | ||
| freshness: verified.freshness, | ||
| lifecycle: entry.lifecycle, | ||
| replacement: entry.replacement ?? null, | ||
| policy: { | ||
| id: policyId, | ||
| version: evaluation.policy_version, | ||
| usage: policy.usage, | ||
| transitions: policy.transitions, | ||
| materiality: policy.materiality, | ||
| execution: policy.execution, | ||
| }, | ||
| binding: { | ||
| id: bindingId, | ||
| version: evaluation.binding_version, | ||
| host: preview.composition.host, | ||
| profiles: agents.profiles, | ||
| dispatch: preview.composition.dispatch, | ||
| }, | ||
| compatibility: { | ||
| hosts: entry.compatible_hosts, | ||
| minPlanrVersion: entry.min_planr_version, | ||
| maxPlanrVersion: entry.max_planr_version, | ||
| }, | ||
| enforcement: [ | ||
| { | ||
| dimension: "Policy limits", | ||
| state: "verified", | ||
| detail: "Planr validates count, time, concurrency, transition, and safety-stop limits before dispatch.", | ||
| }, | ||
| { | ||
| dimension: "Execution permissions", | ||
| state: "verified", | ||
| detail: "Registry-safe policy: no commands, hooks, network/MCP grants, secrets, or overwrite permission.", | ||
| }, | ||
| { | ||
| dimension: "Model and effort", | ||
| state: "host_enforced", | ||
| detail: "The host binding requests concrete routes; the host retains final execution authority.", | ||
| }, | ||
| { | ||
| dimension: "Effective route evidence", | ||
| state: runs > 0 && verifiedRoutes === runs ? "verified" : "unavailable", | ||
| detail: `${verifiedRoutes} of ${runs} evaluation runs carried verified effective-route evidence.`, | ||
| }, | ||
| ], | ||
| evaluation: { | ||
| suiteId: report.suite?.id, | ||
| suiteVersion: report.suite?.version, | ||
| evaluatedAtUnix: report.suite?.evaluated_at_unix, | ||
| reviewAtUnix: entry.review_at_unix, | ||
| status: candidate.status, | ||
| metrics: candidate.metrics, | ||
| thresholds: candidate.threshold_results, | ||
| resultHashes: candidate.results?.map((result) => result.result_sha256) ?? [], | ||
| fixtureSha256: report.suite?.fixture_sha256, | ||
| }, | ||
| registry: { | ||
| id: verified.registry_id, | ||
| version: verified.registry_version, | ||
| manifestSha256: verified.manifest_sha256, | ||
| signer: entry.signature?.signer, | ||
| signatureVerified: true, | ||
| trustedMaintainer: true, | ||
| artifacts: [policyArtifact, bindingArtifact, verificationArtifact].map((artifact) => ({ | ||
| path: artifact.path, | ||
| kind: artifact.kind, | ||
| sha256: artifact.sha256, | ||
| sizeBytes: artifact.size_bytes, | ||
| })), | ||
| }, | ||
| command: previewCommand(policyId, bindingId), | ||
| }; | ||
| } | ||
| export function visibleCompositions(catalog, recommendedOnly = false) { | ||
| const compositions = Array.isArray(catalog?.compositions) ? catalog.compositions : []; | ||
| return recommendedOnly ? compositions.filter((entry) => entry.recommended) : compositions; | ||
| } |
| import assert from "node:assert/strict"; | ||
| import test from "node:test"; | ||
| import { | ||
| formatBasisPoints, | ||
| previewCommand, | ||
| projectComposition, | ||
| safeIdentifier, | ||
| visibleCompositions, | ||
| } from "./catalog-model.mjs"; | ||
| function fixture() { | ||
| const verified = { | ||
| registry_id: "official", | ||
| registry_version: "2026.07", | ||
| manifest_sha256: "a".repeat(64), | ||
| integrity_verified: true, | ||
| signature_verified: true, | ||
| trusted_maintainer: true, | ||
| compatible: true, | ||
| freshness: "current", | ||
| effective_status: "recommended", | ||
| recommended: true, | ||
| entry: { | ||
| id: "balanced-codex", | ||
| version: "1.0.0", | ||
| lifecycle: "published", | ||
| compatible_hosts: ["codex"], | ||
| min_planr_version: "1.3.0", | ||
| max_planr_version: "1.9.0", | ||
| review_at_unix: 1815523200, | ||
| evaluation: { | ||
| policy_id: "balanced", | ||
| policy_version: "1.0.0", | ||
| binding_id: "codex-openai", | ||
| binding_version: "1.0.0", | ||
| }, | ||
| signature: { signer: "planr-maintainers" }, | ||
| artifacts: [ | ||
| { path: "pack/policy.toml", kind: "policy", sha256: "1".repeat(64), size_bytes: 1 }, | ||
| { path: "pack/binding.toml", kind: "host-binding", sha256: "2".repeat(64), size_bytes: 2 }, | ||
| { path: "pack/verification.json", kind: "verification", sha256: "3".repeat(64), size_bytes: 3 }, | ||
| ], | ||
| }, | ||
| }; | ||
| const policy = { | ||
| id: "balanced", | ||
| usage: { max_active_agents: 3, max_parallel_writers: 1, max_depth: 1, metering: "trusted" }, | ||
| transitions: { retry: { max_same_route_retries: 1 }, safety_stop: { enabled: true } }, | ||
| materiality: { changed_files_threshold: 10 }, | ||
| execution: { roles: { worker: { commands: [], hooks: [], network_hosts: [], mcp_servers: [] } } }, | ||
| }; | ||
| const preview = { | ||
| pack: { safe: true }, | ||
| composition: { host: "codex", binding: { id: "codex-openai" }, dispatch: {} }, | ||
| artifacts: [ | ||
| { kind: "active_policy", config_diff: { proposed: { value: policy } } }, | ||
| { kind: "agent_registry", config_diff: { proposed: { value: { profiles: {} } } } }, | ||
| ], | ||
| }; | ||
| const candidate = { | ||
| policy: { id: "balanced" }, | ||
| binding: { id: "codex-openai" }, | ||
| status: "recommended", | ||
| metrics: { runs: 7, verified_route_runs: 7, average_quality_score_bps: 9600 }, | ||
| threshold_results: [{ name: "quality", pass: true }], | ||
| results: [{ result_sha256: "4".repeat(64) }], | ||
| }; | ||
| const verificationEnvelope = { | ||
| report: { | ||
| suite: { id: "planr-preset-suite", version: "1.8.0", evaluated_at_unix: 1783987200, fixture_sha256: "5".repeat(64) }, | ||
| candidates: [candidate], | ||
| recommended: [{ policy: "balanced", binding: "codex-openai", status: "recommended" }], | ||
| }, | ||
| }; | ||
| return { verified, preview, verificationEnvelope }; | ||
| } | ||
| test("projects only trusted, safe, evidence-bound registry entries", () => { | ||
| const projected = projectComposition(fixture()); | ||
| assert.equal(projected.status, "recommended"); | ||
| assert.equal(projected.registry.signatureVerified, true); | ||
| assert.equal(projected.enforcement.at(-1).state, "verified"); | ||
| assert.equal(projected.command, "planr agents preset apply balanced --binding codex-openai"); | ||
| }); | ||
| test("refuses unsigned metadata and recommendation drift", () => { | ||
| const unsigned = fixture(); | ||
| unsigned.verified.signature_verified = false; | ||
| assert.throws(() => projectComposition(unsigned), /trusted maintainer/); | ||
| const drifted = fixture(); | ||
| drifted.verificationEnvelope.report.recommended = []; | ||
| assert.throws(() => projectComposition(drifted), /does not match/); | ||
| }); | ||
| test("publishes lifecycle-demoted recommendations with visible replacement metadata", () => { | ||
| const stale = fixture(); | ||
| stale.verified.freshness = "stale"; | ||
| stale.verified.effective_status = "stale"; | ||
| stale.verified.recommended = false; | ||
| const staleProjected = projectComposition(stale); | ||
| assert.equal(staleProjected.status, "stale"); | ||
| assert.equal(staleProjected.recommended, false); | ||
| const deprecated = fixture(); | ||
| deprecated.verified.effective_status = "deprecated"; | ||
| deprecated.verified.recommended = false; | ||
| deprecated.verified.entry.lifecycle = "deprecated"; | ||
| deprecated.verified.entry.replacement = "balanced-codex-v2"; | ||
| const deprecatedProjected = projectComposition(deprecated); | ||
| assert.equal(deprecatedProjected.status, "deprecated"); | ||
| assert.equal(deprecatedProjected.replacement, "balanced-codex-v2"); | ||
| }); | ||
| test("copy commands accept identifiers only and filtering is deterministic", () => { | ||
| assert.equal(previewCommand("balanced", "codex-openai"), "planr agents preset apply balanced --binding codex-openai"); | ||
| assert.throws(() => safeIdentifier("balanced; curl evil"), /safe registry identifier/); | ||
| assert.deepEqual( | ||
| visibleCompositions({ compositions: [{ recommended: true }, { recommended: false }] }, true), | ||
| [{ recommended: true }], | ||
| ); | ||
| assert.equal(formatBasisPoints(9600), "96.00%"); | ||
| assert.equal(formatBasisPoints(undefined), "—"); | ||
| }); |
| import assert from "node:assert/strict"; | ||
| import { existsSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { dirname, join, resolve } from "node:path"; | ||
| import { spawnSync } from "node:child_process"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import test from "node:test"; | ||
| import { cloudflareTestSteps } from "../scripts/cloudflare-test.mjs"; | ||
| test("launcher keeps deploy and destroy pinned to the test stage", () => { | ||
| assert.deepEqual(cloudflareTestSteps("deploy"), [ | ||
| ["pnpm", ["site:check"]], | ||
| ["pnpm", ["exec", "alchemy", "deploy", "--stage", "test"]], | ||
| ]); | ||
| assert.deepEqual(cloudflareTestSteps("destroy"), [ | ||
| ["pnpm", ["exec", "alchemy", "destroy", "--stage", "test"]], | ||
| ]); | ||
| assert.throws(() => cloudflareTestSteps("prod"), /usage:/); | ||
| }); | ||
| const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); | ||
| const launcher = join(repositoryRoot, "scripts", "cloudflare-test.mjs"); | ||
| for (const version of ["v18.16.1", "v20.19.5"]) { | ||
| const node = join(homedir(), ".nvm", "versions", "node", version, "bin", "node"); | ||
| test( | ||
| `direct launcher rejects ${version} before invoking pnpm`, | ||
| { skip: !existsSync(node) && `local ${version} runtime is unavailable` }, | ||
| () => { | ||
| const result = spawnSync(node, [launcher, "deploy"], { | ||
| cwd: repositoryRoot, | ||
| encoding: "utf8", | ||
| }); | ||
| assert.equal(result.status, 1); | ||
| assert.match(result.stderr, /Cloudflare deployment requires Node\.js 22 or newer/); | ||
| assert.doesNotMatch(result.stderr, /Corepack|ERR_VM|addAbortListener|experimental-strip-types/); | ||
| }, | ||
| ); | ||
| } |
| { | ||
| "schemaVersion": 1, | ||
| "generatedAtUnix": 1783987200, | ||
| "source": { | ||
| "state": "verified_registry_projection", | ||
| "manifest": "website/registry/manifest.toml", | ||
| "entryCount": 1, | ||
| "trust": "planr_registry_v1" | ||
| }, | ||
| "compositions": [ | ||
| { | ||
| "id": "balanced-codex@1.0.0", | ||
| "entryId": "balanced-codex", | ||
| "entryVersion": "1.0.0", | ||
| "status": "recommended", | ||
| "statusLabel": "Recommended", | ||
| "recommended": true, | ||
| "freshness": "current", | ||
| "lifecycle": "published", | ||
| "replacement": null, | ||
| "policy": { | ||
| "id": "balanced", | ||
| "version": "1.0.0", | ||
| "usage": { | ||
| "budget_exhaustion": "stop", | ||
| "max_active_agents": 3, | ||
| "max_attempts": 4, | ||
| "max_depth": 1, | ||
| "max_parallel_readers": 2, | ||
| "max_parallel_writers": 1, | ||
| "max_tool_calls": 120, | ||
| "max_wall_time_seconds": 3600, | ||
| "metering": "estimated", | ||
| "review_reserve_percent": 20 | ||
| }, | ||
| "transitions": { | ||
| "availability_fallback": { | ||
| "max_fallbacks": 1, | ||
| "require_same_capability_class": true | ||
| }, | ||
| "quality_escalation": { | ||
| "max_escalations": 1, | ||
| "require_verification_evidence": true | ||
| }, | ||
| "quota_downgrade": { | ||
| "enabled": false, | ||
| "max_downgrades": 0, | ||
| "noncritical_only": true | ||
| }, | ||
| "retry": { | ||
| "max_same_route_retries": 1 | ||
| }, | ||
| "safety_stop": { | ||
| "enabled": true | ||
| } | ||
| }, | ||
| "materiality": { | ||
| "changed_files_threshold": 10, | ||
| "changed_lines_threshold": 500 | ||
| }, | ||
| "execution": { | ||
| "max_read_scope_entries": 8, | ||
| "max_write_scope_entries": 4, | ||
| "roles": { | ||
| "worker": { | ||
| "approvals": [], | ||
| "commands": [], | ||
| "environment": [], | ||
| "filesystem": { | ||
| "allow_overwrite": false, | ||
| "read_roots": [ | ||
| "docs", | ||
| "src", | ||
| "tests" | ||
| ], | ||
| "write_roots": [ | ||
| "docs", | ||
| "src", | ||
| "tests" | ||
| ] | ||
| }, | ||
| "hooks": [], | ||
| "mcp_servers": [], | ||
| "network_hosts": [], | ||
| "secret_references": "[REDACTED]", | ||
| "tools": [ | ||
| "cargo", | ||
| "git", | ||
| "rg" | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "binding": { | ||
| "id": "codex-openai", | ||
| "version": "1.0.0", | ||
| "host": "codex", | ||
| "profiles": { | ||
| "codex-openai-driver": { | ||
| "capabilities": [], | ||
| "client": "codex", | ||
| "cost_tier": "premium", | ||
| "effort": "xhigh", | ||
| "model": "gpt-5.5" | ||
| }, | ||
| "codex-openai-worker": { | ||
| "capabilities": [], | ||
| "client": "codex", | ||
| "cost_tier": "standard", | ||
| "effort": "high", | ||
| "model": "gpt-5.4-mini", | ||
| "skill": "planr-work" | ||
| } | ||
| }, | ||
| "dispatch": { | ||
| "driver": { | ||
| "capability_evidence": [ | ||
| "builtin-codex-none-fork-contract" | ||
| ], | ||
| "effort_override": false, | ||
| "fork_turns": { | ||
| "mode": "none" | ||
| }, | ||
| "model_override": false | ||
| }, | ||
| "worker": { | ||
| "capability_evidence": [ | ||
| "builtin-codex-none-fork-contract" | ||
| ], | ||
| "effort_override": true, | ||
| "fork_turns": { | ||
| "mode": "none" | ||
| }, | ||
| "model_override": true | ||
| } | ||
| } | ||
| }, | ||
| "compatibility": { | ||
| "hosts": [ | ||
| "codex" | ||
| ], | ||
| "minPlanrVersion": "1.3.0", | ||
| "maxPlanrVersion": "1.9.0" | ||
| }, | ||
| "enforcement": [ | ||
| { | ||
| "dimension": "Policy limits", | ||
| "state": "verified", | ||
| "detail": "Planr validates count, time, concurrency, transition, and safety-stop limits before dispatch." | ||
| }, | ||
| { | ||
| "dimension": "Execution permissions", | ||
| "state": "verified", | ||
| "detail": "Registry-safe policy: no commands, hooks, network/MCP grants, secrets, or overwrite permission." | ||
| }, | ||
| { | ||
| "dimension": "Model and effort", | ||
| "state": "host_enforced", | ||
| "detail": "The host binding requests concrete routes; the host retains final execution authority." | ||
| }, | ||
| { | ||
| "dimension": "Effective route evidence", | ||
| "state": "verified", | ||
| "detail": "7 of 7 evaluation runs carried verified effective-route evidence." | ||
| } | ||
| ], | ||
| "evaluation": { | ||
| "suiteId": "planr-preset-suite", | ||
| "suiteVersion": "1.8.0", | ||
| "evaluatedAtUnix": 1783987200, | ||
| "reviewAtUnix": 1815523200, | ||
| "status": "recommended", | ||
| "metrics": { | ||
| "actual_task_runs": 7, | ||
| "availability_fallbacks": 7, | ||
| "average_credits_micros": 100, | ||
| "average_quality_score_bps": 10000, | ||
| "corrections": 7, | ||
| "metrics_source": "trusted_telemetry", | ||
| "oracle_passes": 7, | ||
| "p95_latency_ms": 228, | ||
| "quality_escalations": 7, | ||
| "reliability_bps": 10000, | ||
| "retries": 7, | ||
| "runs": 7, | ||
| "safety_stop_attempts": 1, | ||
| "safety_stop_failures": 0, | ||
| "total_credits_micros": 700, | ||
| "total_tokens": 70, | ||
| "total_tool_calls": 7, | ||
| "transition_contract_failures": 0, | ||
| "verified_result_hashes": 7, | ||
| "verified_route_runs": 7, | ||
| "violations": 0 | ||
| }, | ||
| "thresholds": [ | ||
| { | ||
| "actual": "7", | ||
| "name": "minimum_runs", | ||
| "pass": true, | ||
| "rule": ">= 7" | ||
| }, | ||
| { | ||
| "actual": "10000", | ||
| "name": "quality", | ||
| "pass": true, | ||
| "rule": ">= 8500 bps" | ||
| }, | ||
| { | ||
| "actual": "10000", | ||
| "name": "reliability", | ||
| "pass": true, | ||
| "rule": ">= 9000 bps" | ||
| }, | ||
| { | ||
| "actual": "100", | ||
| "name": "average_cost", | ||
| "pass": true, | ||
| "rule": "<= 1200000 credits_micros" | ||
| }, | ||
| { | ||
| "actual": "228", | ||
| "name": "p95_latency", | ||
| "pass": true, | ||
| "rule": "<= 5000 ms" | ||
| }, | ||
| { | ||
| "actual": "0", | ||
| "name": "transition_contract", | ||
| "pass": true, | ||
| "rule": "<= 0 failures" | ||
| }, | ||
| { | ||
| "actual": "0", | ||
| "name": "safety_stop", | ||
| "pass": true, | ||
| "rule": "<= 0 failures" | ||
| }, | ||
| { | ||
| "actual": "7/7", | ||
| "name": "actual_task_runs", | ||
| "pass": true, | ||
| "rule": "all fixtures executed" | ||
| }, | ||
| { | ||
| "actual": "7/7", | ||
| "name": "output_oracles", | ||
| "pass": true, | ||
| "rule": "all task output oracles pass" | ||
| }, | ||
| { | ||
| "actual": "7/7", | ||
| "name": "route_evidence", | ||
| "pass": true, | ||
| "rule": "all runs verified" | ||
| }, | ||
| { | ||
| "actual": "7/7", | ||
| "name": "result_hashes", | ||
| "pass": true, | ||
| "rule": "all result hashes verified" | ||
| } | ||
| ], | ||
| "resultHashes": [ | ||
| "c721c30da6e5f988de6741e8d4e611f7f45ad36425559a6b67ae4723869619be", | ||
| "e026c322f1f094f7241d352a8e9c70f10b89d456e7f42825b3c689dc5d785dc9", | ||
| "c74bf8de8296ff94ba45deeac158b619e3eb99c4e1b5251169bc2f1586518979", | ||
| "f76f06ae46bc2ce05bd631ee1f702e348a8dd3b8d4b2e53d59c409af4f330015", | ||
| "2165057d66dcc73722a57d01fb237a73f4da12bc70ab8757c817975925bfbbd8", | ||
| "052de2acf92d0ac000a15b92b26d5f173fe4ff606ee1dbe37df1e4a535cbdff8", | ||
| "41bf6ab560a23c450e9def8944c934091b598d270978fef8aaae0933f806619e" | ||
| ], | ||
| "fixtureSha256": "733c94e049d784eec2fa222f60c230e3f185ccefc84c23c313b1d7e85b60ab22" | ||
| }, | ||
| "registry": { | ||
| "id": "planr-official", | ||
| "version": "2026.07", | ||
| "manifestSha256": "1512df7f667dd1c8f92b2eb39a06739336048dde58327195ada85b419a6cb4c7", | ||
| "signer": "planr-release-2026", | ||
| "signatureVerified": true, | ||
| "trustedMaintainer": true, | ||
| "artifacts": [ | ||
| { | ||
| "path": "presets/policies/balanced.toml", | ||
| "kind": "policy", | ||
| "sha256": "827667b678224f6b09f7c3faab188ede4c7bad49cd2fc9fa8e46f01d61147080", | ||
| "sizeBytes": 993 | ||
| }, | ||
| { | ||
| "path": "presets/bindings/codex-openai.toml", | ||
| "kind": "host-binding", | ||
| "sha256": "2f1f1d91000aff8e6a7424f0380c16c46d24ee38c94de5a9beee2158c0bf6575", | ||
| "sizeBytes": 1449 | ||
| }, | ||
| { | ||
| "path": "website/registry/verification.json", | ||
| "kind": "verification", | ||
| "sha256": "7d1e36372b050ecab5080ea029fd3594f8f8f1df2f1d391c36aebd0fb56cf99a", | ||
| "sizeBytes": 276027 | ||
| } | ||
| ] | ||
| }, | ||
| "command": "planr agents preset apply balanced --binding codex-openai" | ||
| } | ||
| ] | ||
| } |
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <meta | ||
| name="description" | ||
| content="Compare verified Planr policy and host-binding compositions with evaluation and registry provenance." | ||
| /> | ||
| <meta | ||
| http-equiv="Content-Security-Policy" | ||
| content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" | ||
| /> | ||
| <title>Planr Preset Catalog</title> | ||
| <link rel="stylesheet" href="./styles.css" /> | ||
| <script type="module" src="./app.mjs"></script> | ||
| </head> | ||
| <body> | ||
| <a class="skip-link" href="#catalog">Skip to catalog</a> | ||
| <header class="site-header"> | ||
| <a class="brand" href="./" aria-label="Planr preset catalog home"> | ||
| <span class="brand-mark" aria-hidden="true">P</span> | ||
| <span>Planr <strong>Presets</strong></span> | ||
| </a> | ||
| <p class="header-note">Evidence before recommendation.</p> | ||
| </header> | ||
| <main id="catalog"> | ||
| <section class="hero" aria-labelledby="hero-title"> | ||
| <div class="hero-copy"> | ||
| <p class="eyebrow">Public catalog · read-only</p> | ||
| <h1 id="hero-title">Choose policy intent.<br />See host reality.</h1> | ||
| <p class="lede"> | ||
| Compare portable Planr policies with versioned host bindings. Every published status comes from | ||
| registry verification and evaluation evidence—not editorial judgment. | ||
| </p> | ||
| </div> | ||
| <dl class="catalog-stats" aria-label="Catalog summary"> | ||
| <div><dt>Published</dt><dd id="stat-published">—</dd></div> | ||
| <div><dt>Recommended</dt><dd id="stat-recommended">—</dd></div> | ||
| <div><dt>Trust source</dt><dd id="stat-trust">Loading</dd></div> | ||
| </dl> | ||
| </section> | ||
| <section id="fixture-banner" class="fixture-banner" hidden aria-label="Test fixture notice"> | ||
| Local test fixture. Recommendation labels on this page are synthetic and are not published registry metadata. | ||
| </section> | ||
| <section class="authority-note" aria-labelledby="authority-title"> | ||
| <div aria-hidden="true" class="authority-icon">↳</div> | ||
| <div> | ||
| <h2 id="authority-title">Planr governs policy; the host executes the route.</h2> | ||
| <p> | ||
| A binding can request a model, effort, and context-fork mode. Codex, Claude Code, Cursor, or another host | ||
| retains the final concrete execution choice. The catalog distinguishes binding intent from verified | ||
| effective-route evidence. | ||
| </p> | ||
| </div> | ||
| </section> | ||
| <section class="compare-shell" aria-labelledby="compare-title"> | ||
| <div class="section-heading"> | ||
| <div> | ||
| <p class="eyebrow">Side-by-side</p> | ||
| <h2 id="compare-title">Compare compositions</h2> | ||
| </div> | ||
| <label class="toggle"> | ||
| <input id="recommended-only" type="checkbox" /> | ||
| <span>Recommended only</span> | ||
| </label> | ||
| </div> | ||
| <div class="selectors" aria-label="Comparison choices"> | ||
| <label> | ||
| <span>Composition A</span> | ||
| <select id="composition-a" aria-controls="comparison-a"></select> | ||
| </label> | ||
| <span class="versus" aria-hidden="true">vs</span> | ||
| <label> | ||
| <span>Composition B</span> | ||
| <select id="composition-b" aria-controls="comparison-b"></select> | ||
| </label> | ||
| </div> | ||
| <div id="empty-state" class="empty-state" hidden> | ||
| <p class="eyebrow">Publication gate active</p> | ||
| <h3>No trusted compositions are published.</h3> | ||
| <p id="empty-message"> | ||
| Generate this catalog from a signed registry manifest after Planr verifies its evaluation evidence. | ||
| </p> | ||
| <code>node website/build-catalog.mjs --help</code> | ||
| </div> | ||
| <div id="comparison" class="comparison-grid" aria-live="polite"> | ||
| <article id="comparison-a" class="preset-card" aria-label="Composition A"></article> | ||
| <article id="comparison-b" class="preset-card" aria-label="Composition B"></article> | ||
| </div> | ||
| </section> | ||
| <section class="legend" aria-labelledby="legend-title"> | ||
| <div> | ||
| <p class="eyebrow">Status language</p> | ||
| <h2 id="legend-title">Five states, no ambiguity</h2> | ||
| </div> | ||
| <dl> | ||
| <div><dt>Experimental</dt><dd>Valid enough to inspect; evaluation binding is not required.</dd></div> | ||
| <div><dt>Verified</dt><dd>Integrity and current evaluation checks pass; recommendation gates did not.</dd></div> | ||
| <div><dt>Recommended</dt><dd>Trusted signature plus current canonical evaluation and telemetry gates pass.</dd></div> | ||
| <div><dt>Stale</dt><dd>The review date or current suite validity has expired.</dd></div> | ||
| <div><dt>Deprecated</dt><dd>Published for continuity with an explicit replacement path.</dd></div> | ||
| </dl> | ||
| </section> | ||
| </main> | ||
| <footer> | ||
| <p>Local-first by design. A catalog outage never affects installed packs or active projects.</p> | ||
| <p id="generated-at">Catalog provenance loading…</p> | ||
| </footer> | ||
| <div id="copy-status" class="sr-only" aria-live="polite"></div> | ||
| </body> | ||
| </html> | ||
| # Planr Preset Catalog | ||
| This directory is a dependency-free static website. Production catalog data is never authored by hand: `build-catalog.mjs` invokes Planr's canonical registry verifier, requires a trusted maintainer signature and safe policy/binding preview, binds status to the evaluation report, and writes `data/catalog.json`. | ||
| The committed catalog is generated from the included `registry/manifest.toml`, canonical `registry/verification.json`, and separately pinned public maintainer trust store. Its private release and telemetry signing keys are intentionally not committed. If verification expires, is deprecated, or no longer earns recommendation, the generated catalog preserves the visible lifecycle state instead of inventing or retaining a recommendation. | ||
| ```sh | ||
| node website/build-catalog.mjs \ | ||
| --planr-bin ./target/release/planr \ | ||
| --manifest website/registry/manifest.toml \ | ||
| --content-root . \ | ||
| --trust-store website/registry/trusted-maintainers.toml \ | ||
| --entry balanced-codex=codex \ | ||
| --at-unix 1783987200 \ | ||
| --output website/data/catalog.json | ||
| npm run site:test | ||
| npm run site:serve | ||
| ``` | ||
| ## Cloudflare test deployment | ||
| Cloudflare infrastructure is owned by the repository-root `alchemy.run.ts`. The | ||
| deployment publishes a clean `dist/website` containing only runtime assets; tests, | ||
| fixtures, registry inputs, trust stores, and build tooling are never uploaded. | ||
| Install the pinned development dependency and authenticate Cloudflare using an existing | ||
| Alchemy/Cloudflare profile, Wrangler login, or a least-privilege API token. Set the | ||
| account id from `.env.example` in the process environment when account inference would | ||
| be ambiguous. Planr never runs login/configure commands and never changes user-level | ||
| configuration. This static stack has no secret bindings, so it deliberately requires no | ||
| `ALCHEMY_PASSWORD` or repository-local secret file. | ||
| The published Planr CLI retains its declared Node.js 18+ compatibility. Repository | ||
| dependency installation and the separate Cloudflare deployment toolchain require Node.js | ||
| 22+ because the pinned pnpm/Alchemy lockfile contains current tooling with that minimum. | ||
| Select Node 22 or newer before invoking pnpm. On an unknown machine, this pnpm-free | ||
| preflight is safe to run first even with Node 18 or 20: | ||
| ```sh | ||
| node scripts/check-alchemy-runtime.mjs | ||
| ``` | ||
| The authoritative deployment launcher performs the same check before it starts pnpm or | ||
| Alchemy, so older runtimes receive Planr's error instead of a Corepack or dependency crash. | ||
| ```sh | ||
| pnpm install | ||
| pnpm site:check | ||
| node scripts/cloudflare-test.mjs deploy | ||
| ``` | ||
| After Node 22+ is active, `pnpm deploy:test` is an equivalent convenience alias. | ||
| The committed `pnpm-workspace.yaml` is a repository-only dependency security policy: | ||
| it allows the `esbuild` and `workerd` install scripts required by Alchemy and explicitly | ||
| keeps the unused transitive `sharp` build disabled. It does not modify global pnpm state. | ||
| `deploy:test` always targets the isolated Alchemy stage `test`. It creates a standalone | ||
| Cloudflare Worker website and prints its Cloudflare-assigned `workers.dev` URL. The | ||
| initial deployment deliberately configures no custom domain, DNS route, backend resource, | ||
| or adoption of an existing Worker. | ||
| Removing that test website is an explicit, separate operation: | ||
| ```sh | ||
| node scripts/cloudflare-test.mjs destroy | ||
| ``` | ||
| After Node 22+ is active, `pnpm destroy:test` is an equivalent convenience alias. | ||
| Never add private registry or telemetry signing keys to the deployment environment. The | ||
| site deploy consumes only the already verified public `website/data/catalog.json`. | ||
| Release operators must regenerate the evaluation report through `planr agents preset evaluate` with an independently pinned telemetry collector, update the manifest artifact digest, and sign the registry entry with the corresponding offline maintainer key before rebuilding this file. The public `registry/report.md` summarizes that evidence; the machine report and signature remain the verification source of truth. | ||
| The report-wide `reproducible_evidence` flag summarizes the entire candidate matrix. Registry publication independently requires the selected candidate's complete reproducible evidence, passing thresholds, verified route receipts, and matching entry in `report.recommended`; candidates that fail those gates are not published as recommendations. | ||
| The localhost-only `?fixture=recommended` query remains available for isolated UI regression checks. It is visibly labeled, ignored on non-local hosts, and never substitutes for live verification of the production `data/catalog.json` path. |
| schema_version = 1 | ||
| id = "planr-official" | ||
| version = "2026.07" | ||
| generated_at_unix = 1783987200 | ||
| [[entries]] | ||
| id = "balanced-codex" | ||
| version = "1.0.0" | ||
| kind = "pack" | ||
| lifecycle = "published" | ||
| verification_status = "recommended" | ||
| verified_at_unix = 1783987200 | ||
| review_at_unix = 1815523200 | ||
| compatible_hosts = ["codex"] | ||
| min_planr_version = "1.3.0" | ||
| max_planr_version = "1.9.0" | ||
| verification_path = "website/registry/verification.json" | ||
| [entries.evaluation] | ||
| policy_id = "balanced" | ||
| policy_version = "1.0.0" | ||
| binding_id = "codex-openai" | ||
| binding_version = "1.0.0" | ||
| suite_id = "planr-preset-suite" | ||
| suite_version = "1.8.0" | ||
| [[entries.artifacts]] | ||
| path = "presets/policies/balanced.toml" | ||
| kind = "policy" | ||
| sha256 = "827667b678224f6b09f7c3faab188ede4c7bad49cd2fc9fa8e46f01d61147080" | ||
| size_bytes = 993 | ||
| [[entries.artifacts]] | ||
| path = "presets/bindings/codex-openai.toml" | ||
| kind = "host-binding" | ||
| sha256 = "2f1f1d91000aff8e6a7424f0380c16c46d24ee38c94de5a9beee2158c0bf6575" | ||
| size_bytes = 1449 | ||
| [[entries.artifacts]] | ||
| path = "website/registry/verification.json" | ||
| kind = "verification" | ||
| sha256 = "7d1e36372b050ecab5080ea029fd3594f8f8f1df2f1d391c36aebd0fb56cf99a" | ||
| size_bytes = 276027 | ||
| [entries.signature] | ||
| signer = "planr-release-2026" | ||
| algorithm = "ed25519" | ||
| value = "122e213bab8e51b3030685a48b1a3520bc5e2397515dc3aa38da7ed333211e8202e4802949e667add6f29389af29510c2570c4511385206e61c17d4aa914d208" |
| # Preset Evaluation Verification | ||
| Fixture: `planr-preset-suite` v1.8.0 (`733c94e049d784eec2fa222f60c230e3f185ccefc84c23c313b1d7e85b60ab22`) | ||
| Verified: 1783987200 / expires: 1815523200 / evaluated: 1783987200 | ||
| Runner: planr-instrumented-live-host-runner schema 1 / Planr 1.3.0 / macos aarch64 | ||
| This report joined Planr-read challenge-bound task artifacts with independently signed Ed25519 telemetry receipts. Verified receipts provide trusted effective-route and usage measurements; missing or invalid receipts remain recommendation-ineligible. | ||
| | Candidate | Status | Source | Quality | Reliability | Avg cost | p95 latency | Hashes | Label | | ||
| |---|---|---|---:|---:|---:|---:|---:|---| | ||
| | `balanced-codex-openai` | `Recommended` | `TrustedTelemetry` | 10000 bps | 10000 bps | 100 μcredits | 228 ms | 7/7 | recommended | | ||
| | `low-usage-codex-openai` | `Unverified` | `TrustedTelemetry` | 9663 bps | 4285 bps | 100 μcredits | 197 ms | 7/7 | — | | ||
| | `max-quality-codex-openai` | `Recommended` | `TrustedTelemetry` | 10000 bps | 10000 bps | 100 μcredits | 171 ms | 7/7 | recommended | | ||
| | `read-only-audit-codex-openai` | `Unverified` | `TrustedTelemetry` | 9831 bps | 7142 bps | 100 μcredits | 169 ms | 7/7 | — | | ||
| ## Versioned task inputs | ||
| - `Exploration` `explore-routing-boundaries` v1.0.0: `277f2f9155f85365324c6ea6cd717baa47bc9c482446217b30d9f94c6d6684df` | ||
| - `Implementation` `implement-bounded-policy-change` v1.0.0: `f4704037e6a63746e100d31aa98843adeca458fdb336a3317565b62693d676d2` | ||
| - `Mechanical` `mechanical-schema-rewrite` v1.0.0: `96882c36d71af8c0637a17cfb47db66dbef0fbd170c963ba500c8d9f23d48a7a` | ||
| - `Browser` `browser-report-smoke` v1.0.0: `fc453ccf8422fa0093edb60b0162e83982e005b07ee569de5dff222b13319827` | ||
| - `Visual` `visual-report-regression` v1.0.0: `d71938957b10cbcf8dd67fde7e43853ba66fc23ab3b436fa3287caa6a2b5a1f3` | ||
| - `Security` `security-safety-stop` v1.0.0: `dd439a0094897e7ebed01c301fed542e910e8e2b7d6b2a5942c1c44b47082e7f` | ||
| - `Subagent` `subagent-sol-luna-dispatch` v1.0.0: `38372ba41431ccf4f2e9b56ea5f93db55e52156e3fa1c2956bf3dca15d63654f` | ||
| ## Codex Sol/Luna contract | ||
| - [x] `fork_turns = all` rejected | ||
| - [x] `fork_turns = none` parameters verified | ||
| - [x] missing effective model/effort cannot verify | ||
| - [x] process-exit effective route evidence verifies | ||
| Reproducible evidence: **fail**. |
| schema_version = 1 | ||
| [[maintainers]] | ||
| id = "planr-release-2026" | ||
| public_key = "e8631493d6da6ea3983f17585270f558e7757da5bb08bd228756da318d0a6b14" | ||
| revoked = false |
Sorry, the diff of this file is too big to display
| #!/usr/bin/env node | ||
| import { createReadStream, statSync } from "node:fs"; | ||
| import { createServer } from "node:http"; | ||
| import { extname, join, normalize, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| const root = resolve(fileURLToPath(new URL(".", import.meta.url))); | ||
| const port = Number(process.env.PORT ?? 4173); | ||
| const types = { | ||
| ".css": "text/css; charset=utf-8", | ||
| ".html": "text/html; charset=utf-8", | ||
| ".json": "application/json; charset=utf-8", | ||
| ".mjs": "text/javascript; charset=utf-8", | ||
| ".svg": "image/svg+xml", | ||
| }; | ||
| const server = createServer((request, response) => { | ||
| const pathname = decodeURIComponent(new URL(request.url, "http://localhost").pathname); | ||
| const relative = normalize(pathname === "/" ? "index.html" : pathname.replace(/^\/+/, "")); | ||
| const path = resolve(join(root, relative)); | ||
| if (!path.startsWith(`${root}/`)) { | ||
| response.writeHead(403).end("Forbidden"); | ||
| return; | ||
| } | ||
| try { | ||
| if (!statSync(path).isFile()) throw new Error("not a file"); | ||
| response.writeHead(200, { | ||
| "Content-Type": types[extname(path)] ?? "application/octet-stream", | ||
| "Cache-Control": "no-store", | ||
| "X-Content-Type-Options": "nosniff", | ||
| }); | ||
| createReadStream(path).pipe(response); | ||
| } catch { | ||
| response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }).end("Not found"); | ||
| } | ||
| }); | ||
| server.listen(port, "127.0.0.1", () => { | ||
| console.log(`Planr preset catalog listening on http://127.0.0.1:${port}`); | ||
| }); | ||
| :root { | ||
| color-scheme: dark; | ||
| --ink: #f4f0e7; | ||
| --muted: #a9a49a; | ||
| --quiet: #77736c; | ||
| --paper: #11110f; | ||
| --panel: #181815; | ||
| --panel-2: #20201c; | ||
| --line: #35342e; | ||
| --acid: #d9ff57; | ||
| --blue: #7bd9ff; | ||
| --amber: #ffc76a; | ||
| --red: #ff8e7d; | ||
| --green: #8fe3a8; | ||
| --radius: 18px; | ||
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | ||
| font-synthesis: none; | ||
| } | ||
| * { box-sizing: border-box; } | ||
| html { scroll-behavior: smooth; } | ||
| body { | ||
| margin: 0; | ||
| background: | ||
| radial-gradient(circle at 85% 5%, rgb(217 255 87 / 8%), transparent 30rem), | ||
| linear-gradient(180deg, #151511 0, var(--paper) 36rem); | ||
| color: var(--ink); | ||
| min-width: 300px; | ||
| } | ||
| button, select, input { font: inherit; } | ||
| button, select { color: inherit; } | ||
| a { color: inherit; } | ||
| .skip-link { | ||
| position: fixed; | ||
| left: 1rem; | ||
| top: 1rem; | ||
| z-index: 10; | ||
| padding: .75rem 1rem; | ||
| background: var(--acid); | ||
| color: #111; | ||
| clip-path: inset(50%); | ||
| height: 1px; | ||
| width: 1px; | ||
| overflow: hidden; | ||
| white-space: nowrap; | ||
| transform: translateY(-200%); | ||
| } | ||
| .skip-link:focus { | ||
| clip-path: none; | ||
| height: auto; | ||
| width: auto; | ||
| overflow: visible; | ||
| transform: translateY(0); | ||
| } | ||
| .site-header, main, footer { width: min(1180px, calc(100% - 2.5rem)); margin-inline: auto; } | ||
| .site-header { | ||
| min-height: 78px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| border-bottom: 1px solid var(--line); | ||
| } | ||
| .brand { display: inline-flex; gap: .7rem; align-items: center; text-decoration: none; letter-spacing: -.02em; } | ||
| .brand-mark { | ||
| display: grid; | ||
| place-items: center; | ||
| width: 2rem; | ||
| height: 2rem; | ||
| border: 1px solid var(--acid); | ||
| border-radius: 50%; | ||
| color: var(--acid); | ||
| font-family: ui-monospace, SFMono-Regular, Menlo, monospace; | ||
| } | ||
| .brand strong { color: var(--acid); font-weight: 650; } | ||
| .header-note { color: var(--quiet); font-size: .85rem; } | ||
| .hero { | ||
| min-height: 440px; | ||
| display: grid; | ||
| grid-template-columns: minmax(0, 1.5fr) minmax(290px, .7fr); | ||
| gap: 5rem; | ||
| align-items: center; | ||
| padding-block: 6rem 4.5rem; | ||
| } | ||
| .eyebrow { | ||
| margin: 0 0 .75rem; | ||
| color: var(--acid); | ||
| font: 600 .72rem/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; | ||
| letter-spacing: .13em; | ||
| text-transform: uppercase; | ||
| } | ||
| h1, h2, h3, p { text-wrap: pretty; } | ||
| h1 { margin: 0; font-size: clamp(3rem, 7vw, 6.5rem); line-height: .91; letter-spacing: -.07em; font-weight: 600; } | ||
| h2 { margin: 0; font-size: clamp(1.6rem, 3vw, 2.65rem); line-height: 1; letter-spacing: -.045em; } | ||
| h3 { letter-spacing: -.025em; } | ||
| .lede { max-width: 720px; margin: 2rem 0 0; color: var(--muted); font-size: clamp(1rem, 1.8vw, 1.25rem); line-height: 1.6; } | ||
| .catalog-stats { margin: 0; border-top: 1px solid var(--line); } | ||
| .catalog-stats div { display: grid; grid-template-columns: 1fr auto; gap: 1rem; padding: 1.15rem 0; border-bottom: 1px solid var(--line); } | ||
| .catalog-stats dt { color: var(--muted); } | ||
| .catalog-stats dd { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } | ||
| .fixture-banner { margin-bottom: 1rem; padding: .9rem 1.1rem; border: 1px solid var(--amber); background: rgb(255 199 106 / 8%); color: var(--amber); border-radius: 12px; } | ||
| .authority-note { | ||
| display: grid; | ||
| grid-template-columns: auto 1fr; | ||
| gap: 1.25rem; | ||
| margin-bottom: 5rem; | ||
| padding: 1.4rem; | ||
| border: 1px solid var(--blue); | ||
| border-radius: var(--radius); | ||
| background: rgb(123 217 255 / 5%); | ||
| } | ||
| .authority-icon { color: var(--blue); font-size: 2rem; line-height: 1; } | ||
| .authority-note h2 { font-size: 1.1rem; letter-spacing: -.015em; } | ||
| .authority-note p { margin: .5rem 0 0; color: var(--muted); line-height: 1.55; max-width: 920px; } | ||
| .compare-shell { padding-block: 2rem 6rem; } | ||
| .section-heading { display: flex; justify-content: space-between; align-items: end; gap: 2rem; } | ||
| .toggle { display: flex; align-items: center; gap: .65rem; color: var(--muted); cursor: pointer; } | ||
| .toggle input { width: 1.15rem; height: 1.15rem; accent-color: var(--acid); } | ||
| .selectors { display: grid; grid-template-columns: 1fr auto 1fr; gap: 1rem; align-items: end; margin: 2.2rem 0 1.2rem; } | ||
| .selectors label { display: grid; gap: .55rem; color: var(--muted); font-size: .8rem; } | ||
| select { | ||
| width: 100%; | ||
| padding: .95rem 2.5rem .95rem 1rem; | ||
| border: 1px solid var(--line); | ||
| border-radius: 12px; | ||
| background: var(--panel); | ||
| } | ||
| select:focus-visible, button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; } | ||
| .versus { padding-bottom: 1rem; color: var(--quiet); font: .75rem ui-monospace, monospace; text-transform: uppercase; } | ||
| .comparison-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.2rem; } | ||
| .preset-card { min-width: 0; padding: clamp(1.2rem, 3vw, 2rem); border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); } | ||
| .card-top { display: flex; align-items: start; justify-content: space-between; gap: 1rem; } | ||
| .card-top h3 { margin: .2rem 0 0; font-size: clamp(1.45rem, 3vw, 2.1rem); } | ||
| .binding-name { color: var(--muted); font-family: ui-monospace, monospace; font-size: .82rem; } | ||
| .badge { display: inline-flex; align-items: center; gap: .4rem; padding: .35rem .55rem; border: 1px solid currentColor; border-radius: 999px; font: 600 .68rem ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em; } | ||
| .badge::before { content: "●"; font-size: .55rem; } | ||
| .status-recommended { color: var(--acid); } | ||
| .status-verified { color: var(--blue); } | ||
| .status-stale, .status-deprecated { color: var(--amber); } | ||
| .status-experimental, .status-unverified { color: var(--muted); } | ||
| .meta-line { color: var(--quiet); font: .75rem/1.5 ui-monospace, monospace; } | ||
| .metric-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; margin: 1.5rem 0; overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: var(--line); } | ||
| .metric { padding: 1rem; background: var(--panel-2); } | ||
| .metric span { display: block; color: var(--quiet); font-size: .72rem; margin-bottom: .4rem; } | ||
| .metric strong { font: 500 1.15rem ui-monospace, monospace; } | ||
| .card-section { padding: 1.25rem 0; border-top: 1px solid var(--line); } | ||
| .card-section h4 { margin: 0 0 .9rem; font-size: .78rem; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; } | ||
| .enforcement-list { display: grid; gap: .7rem; margin: 0; } | ||
| .enforcement-list div { display: grid; grid-template-columns: minmax(120px, .45fr) 1fr; gap: 1rem; } | ||
| .enforcement-list dt { font-size: .82rem; } | ||
| .enforcement-list dd { margin: 0; color: var(--muted); font-size: .82rem; line-height: 1.45; } | ||
| .compatibility-list { display: grid; gap: .7rem; margin: 0; } | ||
| .compatibility-list div { display: grid; grid-template-columns: minmax(120px, .45fr) 1fr; gap: 1rem; } | ||
| .compatibility-list dt { font-size: .82rem; } | ||
| .compatibility-list dd { margin: 0; color: var(--muted); font-size: .82rem; line-height: 1.45; } | ||
| .state { display: inline-block; margin-left: .35rem; color: var(--blue); font: .65rem ui-monospace, monospace; text-transform: uppercase; } | ||
| .hash { overflow-wrap: anywhere; color: var(--quiet); font: .7rem/1.5 ui-monospace, monospace; } | ||
| .command-block { display: grid; gap: .75rem; } | ||
| .command-block code { display: block; overflow-x: auto; padding: 1rem; border-radius: 10px; background: #0b0b09; color: var(--acid); font: .75rem/1.5 ui-monospace, monospace; } | ||
| .copy-button { justify-self: start; padding: .7rem .95rem; border: 1px solid var(--acid); border-radius: 9px; background: transparent; color: var(--acid); cursor: pointer; } | ||
| .copy-button:hover { background: var(--acid); color: #111; } | ||
| .empty-state { padding: 3rem; border: 1px dashed var(--line); border-radius: var(--radius); background: var(--panel); } | ||
| .empty-state h3 { margin: .25rem 0 .75rem; font-size: 1.6rem; } | ||
| .empty-state p:not(.eyebrow) { color: var(--muted); } | ||
| .empty-state code { color: var(--acid); overflow-wrap: anywhere; } | ||
| .legend { display: grid; grid-template-columns: .65fr 1.35fr; gap: 5rem; padding: 5rem 0; border-top: 1px solid var(--line); } | ||
| .legend dl { margin: 0; } | ||
| .legend dl div { display: grid; grid-template-columns: 140px 1fr; gap: 1rem; padding: .8rem 0; border-bottom: 1px solid var(--line); } | ||
| .legend dt { font-weight: 650; } | ||
| .legend dd { margin: 0; color: var(--muted); line-height: 1.5; } | ||
| footer { display: flex; justify-content: space-between; gap: 2rem; padding: 2.5rem 0 4rem; border-top: 1px solid var(--line); color: var(--quiet); font-size: .78rem; } | ||
| .sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } | ||
| @media (max-width: 760px) { | ||
| .site-header, main, footer { width: min(100% - 1.4rem, 1180px); } | ||
| .header-note { display: none; } | ||
| .hero { min-height: auto; grid-template-columns: 1fr; gap: 3rem; padding-block: 4rem 3rem; } | ||
| h1 { font-size: clamp(3rem, 16vw, 5.5rem); } | ||
| .authority-note { margin-bottom: 3rem; } | ||
| .section-heading { align-items: start; flex-direction: column; } | ||
| .selectors { grid-template-columns: 1fr; } | ||
| .versus { display: none; } | ||
| .comparison-grid { grid-template-columns: 1fr; } | ||
| .legend { grid-template-columns: 1fr; gap: 2rem; } | ||
| .legend dl div { grid-template-columns: 110px 1fr; } | ||
| footer { flex-direction: column; } | ||
| } | ||
| @media (prefers-reduced-motion: reduce) { | ||
| html { scroll-behavior: auto; } | ||
| *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } | ||
| } |
| { | ||
| "schemaVersion": 1, | ||
| "generatedAtUnix": 1783987200, | ||
| "source": { | ||
| "state": "verified_registry_projection", | ||
| "entryCount": 2, | ||
| "trust": "localhost_test_fixture", | ||
| "message": "Synthetic browser fixture; never used by the production catalog." | ||
| }, | ||
| "compositions": [ | ||
| { | ||
| "id": "balanced-codex@1.0.0", | ||
| "entryId": "balanced-codex", | ||
| "entryVersion": "1.0.0", | ||
| "status": "recommended", | ||
| "recommended": true, | ||
| "freshness": "current", | ||
| "lifecycle": "published", | ||
| "policy": { | ||
| "id": "balanced", | ||
| "version": "1.0.0", | ||
| "usage": { "max_active_agents": 3, "max_parallel_writers": 1, "max_depth": 1, "metering": "trusted" } | ||
| }, | ||
| "binding": { "id": "codex-openai", "version": "1.0.0", "host": "codex" }, | ||
| "compatibility": { "hosts": ["codex"], "minPlanrVersion": "1.3.0", "maxPlanrVersion": "1.9.0" }, | ||
| "enforcement": [ | ||
| { "dimension": "Policy limits", "state": "verified", "detail": "Planr validates count, time, concurrency, transition, and safety-stop limits before dispatch." }, | ||
| { "dimension": "Execution permissions", "state": "verified", "detail": "No commands, hooks, network/MCP grants, secret references, or overwrite permission." }, | ||
| { "dimension": "Model and effort", "state": "host_enforced", "detail": "Codex retains final concrete execution authority." }, | ||
| { "dimension": "Effective route evidence", "state": "verified", "detail": "7 of 7 evaluation runs carried verified effective-route evidence." } | ||
| ], | ||
| "evaluation": { | ||
| "suiteId": "planr-preset-suite", | ||
| "suiteVersion": "1.8.0", | ||
| "evaluatedAtUnix": 1783987200, | ||
| "reviewAtUnix": 1815523200, | ||
| "metrics": { "average_quality_score_bps": 9600, "runs": 7, "oracle_passes": 7 } | ||
| }, | ||
| "registry": { "manifestSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "signer": "localhost-fixture" } | ||
| }, | ||
| { | ||
| "id": "low-usage-claude@1.0.0", | ||
| "entryId": "low-usage-claude", | ||
| "entryVersion": "1.0.0", | ||
| "status": "recommended", | ||
| "recommended": true, | ||
| "freshness": "current", | ||
| "lifecycle": "published", | ||
| "policy": { | ||
| "id": "low-usage", | ||
| "version": "1.0.0", | ||
| "usage": { "max_active_agents": 2, "max_parallel_writers": 1, "max_depth": 1, "metering": "trusted" } | ||
| }, | ||
| "binding": { "id": "claude-native", "version": "1.0.0", "host": "claude-code" }, | ||
| "compatibility": { "hosts": ["claude-code"], "minPlanrVersion": "1.3.0", "maxPlanrVersion": "1.9.0" }, | ||
| "enforcement": [ | ||
| { "dimension": "Policy limits", "state": "verified", "detail": "Planr validates count, time, concurrency, transition, and safety-stop limits before dispatch." }, | ||
| { "dimension": "Execution permissions", "state": "verified", "detail": "No commands, hooks, network/MCP grants, secret references, or overwrite permission." }, | ||
| { "dimension": "Model and effort", "state": "host_enforced", "detail": "Claude Code retains final concrete execution authority." }, | ||
| { "dimension": "Effective route evidence", "state": "verified", "detail": "7 of 7 evaluation runs carried verified effective-route evidence." } | ||
| ], | ||
| "evaluation": { | ||
| "suiteId": "planr-preset-suite", | ||
| "suiteVersion": "1.8.0", | ||
| "evaluatedAtUnix": 1783987200, | ||
| "reviewAtUnix": 1815523200, | ||
| "metrics": { "average_quality_score_bps": 9100, "runs": 7, "oracle_passes": 7 } | ||
| }, | ||
| "registry": { "manifestSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "signer": "localhost-fixture" } | ||
| } | ||
| ] | ||
| } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
0
-100%3
-70%6
-40%27012743
-15.74%66
-33.33%196
-97.77%163
-22.38%