Browse Source

docs(canonical-refactor): close stage 1 specification gate

darrenhinde 2 weeks ago
parent
commit
b028fb55d9

+ 8 - 10
docs/architecture/canonical-refactor/01-feature-inventory.md

@@ -71,7 +71,7 @@ Measured across all 34 agent `.md` files (`awk` over the YAML block):
 | `mode` | 34 | `primary` \| `subagent`. | 🟢 UNIVERSAL | `role: primary \| subagent`. OpenCode adapter renames `role`→`mode`. |
 | `temperature` | 33 | Sampling temperature (`0`–`0.2` in practice). | 🟡 OPENCODE-SPECIFIC | `inference.temperature`. **Claude Code has no per-agent temperature → degradation warning** (already predicted in `00-INDEX.md`). |
 | `permission` | 24 | Granular per-tool, per-glob allow/ask/deny. | 🟡 OPENCODE-SPECIFIC | `capabilities` (intent). This is the **hardest transform** — see §1.3. |
-| `model` | **0** | — | 🟢 UNIVERSAL | `inference.model: null`. **Confirms Locked Decision #2:** no agent hardcodes a model today. PRs #311/#324 would have *introduced* the problem, not preserved it. |
+| `model` | **0** | — | 🟢 UNIVERSAL | No concrete model is authored. Optional `inference.tier` carries only semantic cost/latency intent. |
 | `tools` | **0** | — | 🟢 UNIVERSAL | OpenCode expresses tool access via `permission`, not `tools`. Claude adapter derives `tools:` allowlist from `capabilities`. |
 | `hooks` | **0** | — | n/a | No agent declares hooks in frontmatter. Hooks exist only as *plugins* (§6) and CC `hooks.json` (§9.2). |
 | `id`,`category`,`type`,`version`,`author` | **1** | Only `eval-runner.md` inlines these. | 🔵 REGISTRY | Everywhere else these live in the `agent-metadata.json` sidecar. `eval-runner` is the **inconsistent outlier**. |
@@ -257,7 +257,7 @@ model: sonnet
 | `description` (multiline + `<example>` blocks) | `description` + `examples[]` | 🟢 | **`examples[]` is a first-class neutral field** — CC folds it into `description`; OpenCode drops it. Today these examples exist **only** in the CC files and would be lost if `.opencode/` is treated as the sole migration source. |
 | `tools` (allowlist) | `capabilities` allow-set | 🟢 | Derived. |
 | `disallowedTools` | `capabilities` deny-set | 🟢 | Derived. |
-| `model: sonnet` | `inference.model` | 🔴 **VIOLATES Locked Decision #2** | CC agents **hardcode `model: sonnet`**. Under `model: null` the build must emit **no** `model:` line. This is a deliberate behavior change — confirm with Open Question **Q3**. |
+| legacy `model:` | migration input → `inference.tier` | 🟡 MIGRATION ONLY | Scouts map to `fast`; other shipped agents map to `balanced`/default. Concrete names are discarded and never enter canonical content. |
 
 > 🔴 **Preservation alert:** the `<example>` blocks in all 7 CC agents are hand-authored content
 > that exists **nowhere in `.opencode/`**. If `/content/` is seeded only from `.opencode/`,
@@ -1157,7 +1157,7 @@ must preserve the escaping** — a naive template would reintroduce the injectio
 
 | Asset | Path | Class | Notes |
 |---|---|---|---|
-| Per-model prompt variants | `.opencode/prompts/core/openagent/{gemini,gpt,grok,llama,minimax,openrouter}.md` | 🔴 AT RISK | **6 model-specific rewrites of the OpenAgent prompt.** Directly tensions Locked Decision #2 (`model: null`). Also `prompts/core/opencoder/{gemini,gpt,grok,llama}.md`. |
+| Per-model prompt variants | `.opencode/prompts/core/openagent/{gemini,gpt,grok,llama,minimax,openrouter}.md` | 🔴 AT RISK | **6 model-specific rewrites of the OpenAgent prompt.** Canonical content cannot select concrete models, so Stage 4 must explicitly merge, retain as target-specific optional assets, or retire them. Also `prompts/core/opencoder/{gemini,gpt,grok,llama}.md`. |
 | Prompt templates | `.opencode/prompts/*/TEMPLATE.md`, `README.md` | 🔵 | |
 | Eval results | `.opencode/prompts/core/openagent/results/*.json`, `default-output.log` | 🔵 | Build artifacts committed to git. |
 | `.opencode/docs/` | `agents/planning-agents-guide.md`, `guides/task-schema-migration.md`, `workflows/full-project-workflow.md` | 🟢 | 3 docs. `planning-agents-guide.md` documents the **unregistered** planning agents (§1.8). |
@@ -1181,7 +1181,7 @@ must preserve the escaping** — a naive template would reintroduce the injectio
 - [ ] `description` → neutral `description`
 - [ ] `mode: primary|subagent` → neutral `role`
 - [ ] `temperature` (33 agents) → `inference.temperature`; **Claude drops it → warning**
-- [ ] `model` stays absent → `inference.model: null` (Locked Decision #2)
+- [ ] Concrete model names stay absent; optional semantic `inference.tier` is preserved (Locked Decision #2)
 - [ ] `permission.<tool>.<glob>` **tri-state** (`allow`/`ask`/`deny`) → `capabilities` **rule list** (§1.3)
 - [ ] **Glob-scoped denies preserved** (`**/*.env*`, `**/*.key`, `**/*.secret`, `node_modules/**`, `.git/**`) — *security control*
 - [ ] **Deny-by-default + narrow allow preserved** (`coder-agent` bash allowlist) — *security control*
@@ -1337,7 +1337,7 @@ must preserve the escaping** — a naive template would reintroduce the injectio
 - [ ] 🔴 `.context-manifest.json` `profile: "standard"` (not a real profile name) fixed
 
 ### Prompts / misc
-- [ ] 🔴 **10 per-model prompt variants** (`openagent/{gemini,gpt,grok,llama,minimax,openrouter}.md`, `opencoder/{gemini,gpt,grok,llama}.md`) — tension with `model: null` (Q23)
+- [ ] 🔴 **10 per-model prompt variants** (`openagent/{gemini,gpt,grok,llama,minimax,openrouter}.md`, `opencoder/{gemini,gpt,grok,llama}.md`) explicitly merged, retained as target-specific optional assets, or retired; they never select a concrete model from canonical content
 - [ ] `prompts/*/TEMPLATE.md` + READMEs preserved
 - [ ] Eval `results/*.json` — keep or gitignore
 - [ ] `.opencode/docs/` (3 docs) relocated
@@ -1364,11 +1364,9 @@ scalar sugar (§1.3) was adopted, and `00-INDEX.md`'s worked example was revised
 flat model was provably lossy against real agents (`coder-agent`'s deny-all-then-allowlist,
 the `**/*.env*`/`**/*.key` security globs, `rm -rf /*: deny`). No longer open.
 
-**Q3 — Model policy vs shipped reality.** Locked Decision #2 says `model: null`. But
-`plugins/claude-code/agents/*.md` hardcode `model: sonnet`, `settings.json` sets `opusplan`
-(PR #264, merged), and `.opencode/prompts/` holds 10 per-model prompt variants. Does `model: null`
-mean (a) **never** emit `model:` for any target, or (b) allow a **per-adapter default** in adapter
-config (not content)? (b) preserves shipped behavior; (a) is a deliberate behavior change.
+**Q3 — CLOSED (v2): no concrete model names in canonical content.** Legacy plugin values are
+migration evidence only: scouts become `inference.tier: fast`, other agents use
+`balanced`/default. Project or adapter configuration may map tiers to target models.
 
 **Q4 — `0-category.json`: promote or delete?** It's read by nothing. Its `icon`/`order`/`status`
 are richer than `registry.json#categories` (flat strings) and worth promoting. But

+ 15 - 9
docs/architecture/canonical-refactor/02-canonical-schema.md

@@ -78,7 +78,8 @@ re-derived in §4.1 as of the same date.
 1. **Intent over syntax.** The IR encodes what the author wants, never a tool's serialization.
 2. **Closed vocabularies for invariants, open strings for hints.** Anything an adapter must reason
    about is a Zod `enum`; free-form author intent stays `string`.
-3. **`null` means "tool default"; absent means "unspecified".** Per locked decision #2.
+3. **Concrete model names are not authored.** `inference.tier` carries optional semantic intent;
+   absent/default tier means the target's configured default. Per locked decision #2.
 4. **Metadata is folded into frontmatter.** No sidecar in `/content/`. The OpenCode *adapter* may
    re-emit `agent-metadata.json`; the IR has no sidecar concept.
 5. **The authored on-disk format and the IR shape do not have to match** (locked decision #6).
@@ -94,6 +95,11 @@ import { z } from "zod";
 export const IdSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
   "id must be kebab-case: lowercase alphanumeric words joined by single hyphens");
 
+/** Backward-compatible imported name. Aliases are lookup keys, not canonical ids, so legacy
+ * PascalCase and underscore names are allowed. New authored ids still use IdSchema. */
+export const AliasSchema = z.string().min(1).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/,
+  "alias must be a non-empty legacy identifier without path separators");
+
 /** Human display name. HINT for casing — adapters slugify/re-case freely. */
 export const NameSchema = z.string().min(1);
 
@@ -593,9 +599,9 @@ the sole owner — closes v2 Q9.)*
 export const AgentSchema = z.object({
   // ---- identity (folded-in metadata; NO sidecar) ----
   id: IdSchema,                              // filename == id (01 Q1, per 06-REVIEW triage)
-  /** Alternate ids this agent answers to in dependency refs (NEW v3 — fixes 06-REVIEW L7).
-   *  Back-compat for renames like tester → test-engineer. */
-  aliases: z.array(IdSchema).default([]),
+  /** Alternate lookup names (NEW v3 — fixes 06-REVIEW L7/L8). Canonical ids stay kebab-case;
+   *  aliases may retain legacy names such as TestEngineer during import. */
+  aliases: z.array(AliasSchema).default([]),
   name: NameSchema,
   description: DescriptionSchema,
   role: RoleSchema,                          // was `mode`
@@ -668,7 +674,7 @@ export const SkillFileSchema = z.object({
 
 export const SkillSchema = z.object({
   id: IdSchema,
-  aliases: z.array(IdSchema).default([]),    // NEW v3
+  aliases: z.array(AliasSchema).default([]), // NEW v3
   name: NameSchema,
   description: DescriptionSchema,
   category: CategorySchema,
@@ -707,7 +713,7 @@ Real source: `.opencode/command/add-context.md`, `context.md`. Verified frontmat
 ```ts
 export const CommandSchema = z.object({
   id: IdSchema,
-  aliases: z.array(IdSchema).default([]),  // NEW v3
+  aliases: z.array(AliasSchema).default([]), // NEW v3
   name: NameSchema.optional(),           // often absent; derive from id
   description: DescriptionSchema,        // NEUTRAL INVARIANT — the only universal field
   tags: TagsSchema,
@@ -865,7 +871,7 @@ string alongside the normalized semver — so a no-op build does not churn ~289
 ```ts
 export const ContextSchema = z.object({
   id: IdSchema,
-  aliases: z.array(IdSchema).default([]),    // NEW v3 — the 3 symlink/dup-id files land here
+  aliases: z.array(AliasSchema).default([]), // NEW v3 — the 3 symlink/dup-id files land here
   /** OPTIONAL/DERIVED (v3 — fixes 06-REVIEW G2 gap 1: v2 required both, but the MVI marker
    *  carries NEITHER a name NOR a description, so every real context file failed
    *  validation). When absent, derived: name = title-cased path basename; description =
@@ -931,7 +937,7 @@ export const ToolParamSchema = z.object({
 
 export const ToolSchema = z.object({
   id: IdSchema,
-  aliases: z.array(IdSchema).default([]),  // NEW v3
+  aliases: z.array(AliasSchema).default([]), // NEW v3
   name: NameSchema,
   description: DescriptionSchema,        // NEUTRAL — the model needs to know what it does
   tags: TagsSchema,
@@ -1045,7 +1051,7 @@ export const RegistryEntrySchema = z.object({
   /** Alternate ids resolvable in dependency refs (RESTORED v3 — 3 live entries use it;
    *  resolution rule in §1.5). Also the landing place for the 3 symlinked standards files
    *  if the symlink→alias collapse is chosen (index v2.2 symlink finding). */
-  aliases: z.array(IdSchema).default([]),
+  aliases: z.array(AliasSchema).default([]),
   name: NameSchema,
   type: ComponentTypeSchema,
   /** Source path under /content/ (NOT .opencode/). NEUTRAL INVARIANT — catalog→source join. */

+ 43 - 98
docs/architecture/canonical-refactor/03-adapter-specs.md

@@ -36,16 +36,17 @@ Per `00-INDEX.md` v2 worked example and `packages/compatibility-layer/src/types.
 | `tags[]` | string[] | Free-form labels. |
 | `capabilities` | see §0.4 | Ordered rule lists + scalar sugar. **INTENT, not any tool's syntax.** |
 | `inference.temperature` | number \| absent | Sampling temperature. |
-| `inference.model` | string \| **null** | `null` ⇒ tool default. **Never hardcode** (locked #2). |
+| `inference.tier` | `fast` \| `balanced` \| `deep` | Semantic cost/latency intent; adapters resolve it to target defaults. Raw model ids are not authorable. |
 | `inference.maxSteps` | number \| absent | Step cap. |
 | `context[]` | `{ path, priority?, description? }` | External context refs. See §0.6. |
-| `dependencies[]` | `subagent:` \| `context:` \| `skill:` \| `command:` \| `tool:` | Declared deps. |
+| `dependencies[]` | typed refs from `02` §0.2 (`agent` through `config`, including path/wildcard refs) | Declared deps. |
 | `examples[]` | `{ context, user, assistant }` | Few-shot delegation examples. |
 | `hooks[]` | `{ event, command, … }` | Lifecycle hooks. |
 | body | markdown | System prompt authored once. |
 
-Verified census (index v2): **34** agents, **20** commands, **296** context `.md`
-(286 HTML-comment / 3 YAML / 7 neither), 2 OpenCode skills, ~11 CC plugin skills.
+Verified census (Stage 1 closure): **34** agents and **20** OpenCode commands. Context composition
+and the **12 + 4 = 16** canonical skill union are recorded from disk in `09-MERGE-RULES.md` §§4,9;
+those measurements supersede the stale index-v2 counts previously printed here.
 
 ### 0.2 Warning contract
 
@@ -66,8 +67,8 @@ degradedFeatureWarning(feature, from, to):
 
 A transform that drops or degrades any field MUST emit a warning AND `analyzeCompatibility()`
 MUST predict it. Builds fail only on `blockers`; warnings are reported and counted.
-**Security-relevant losses (§2.3) are warnings today — see Open Questions #Q3 for whether they
-should block.**
+**Security-relevant losses (§2.3) are build blockers unless the user supplies the explicit
+`--allow-unsafe-degradation` opt-in** (`07-EXECUTION-PLAN.md` Stage 3).
 
 ### 0.4 The capabilities model (Option A — locked decision #5)
 
@@ -84,19 +85,14 @@ transforms. Adapters never see the sugar forms.
 - **Default-scope decision** — the decision of the rule whose scope is `*`, if present. This is
   what coarse targets (CC/Cursor/Windsurf) collapse to.
 - **Exceptions** — every rule whose scope ≠ `*`. These are what coarse targets lose.
-- **Implicit default** — when *no* `*` rule exists. ⚠️ **The IR does not currently define this**,
-  and the two real agents disagree on what it should be: `coder-agent.edit` lists only *denies*
-  (`**/*.env*`, `**/*.key`, …) and clearly means "edit anything except these" ⇒ default
-  **allow**; `code-reviewer.delegate` lists only *allows* (`contextscout`) and clearly means
-  "only this one" ⇒ default **deny**. A workable inference rule is *"implicit default = the
-  opposite of the decisions present; mixed-without-`*` is an error"* — but this is a **schema
-  decision for `02`, not an adapter decision**. Escalated as Open Question #Q2. Every collapse
-  rule below depends on it.
+- **Implicit default** — when no `*` rule exists, infer the opposite of the one decision present:
+  deny-only exceptions imply `allow`; allow-only exceptions imply `deny`; mixed decisions without
+  `*` are a parse error. This is ratified in `02` §1.2.5 and adapters consume that resolved value.
 
-### 0.5 ⚠️ BLOCKING CONTRADICTION — first-match-wins vs last-match-wins
+### 0.5 Resolved precedence — last-match-wins
 
-**The locked shape says "first-match-wins". Every real agent is authored last-match-wins, and
-first-match-wins would break exactly the agents Option A was chosen to protect.**
+Every real agent is authored last-match-wins. `10-PRECEDENCE-EXPERIMENT.md` confirmed that behavior
+against OpenCode 1.17.20 through live probes and the resolver's `.findLast()` implementation.
 
 Verified evidence — `.opencode/agent/subagents/code/coder-agent.md` as authored on disk:
 
@@ -125,22 +121,16 @@ report task status. That is precisely the breakage the index cites as Option A's
 
 Under **last-match-wins**, both files behave exactly as authored and intended.
 
-The repo's own planning doc agrees — `docs/archive/planning/12-MASTER-SYNTHESIS.md:442`:
+The repo's archived planning doc also states the intended rule at
+`docs/archive/planning/12-MASTER-SYNTHESIS.md:432`:
 
 > "The `permission:` field uses **last-match-wins** evaluation (same as OpenCode's native
 > system) … Rules are evaluated in order; the LAST matching rule wins. This matches OpenCode's
 > permission semantics exactly, ensuring IDE-native compatibility."
 
-**The ordered-list decision is right; the stated match direction is wrong.** Two resolutions:
-
-| | Resolution | Consequence |
-|---|---|---|
-| **A (recommended)** | IR adopts **last-match-wins** | Matches OpenCode natively, matches all 34 authored agents **as written**, zero reordering transform, and the index's own `coder-agent` example becomes correct as printed. |
-| B | IR keeps **first-match-wins** | The index's `coder-agent` example must be **reordered specific-first** (it is currently broken as printed); the OpenCode adapter must **reverse rule order** on both serialize and parse; and all 34 agents must be order-reversed at migration. Equivalent expressive power, strictly more machinery and one more place to get it wrong. |
-
-**This doc specifies Resolution A (last-match-wins) throughout.** If the coordinator confirms
-B, §1.3 gains a reversal step and §0.4's "default-scope decision" becomes "the *last* `*` rule".
-Escalated as **Open Question #Q1 — blocking**; it changes generated output for every target.
+**Decision:** the IR and all adapters use last-match-wins. Rule order is preserved; adapters do not
+reverse it. Integer-like and duplicate scopes are rejected at parse time because the live probes
+showed they can change or erase authored precedence before the resolver runs.
 
 ### 0.6 Context on disk (MVI HTML-comment — locked decision #6)
 
@@ -207,8 +197,7 @@ File name = `id` (kebab); `name:` frontmatter = PascalCase. Verified against
 | `capabilities` | `permission:` map | **ordered list → map, order preserved** (§1.3) |
 | `capabilities.delegate` | `permission.task` | `[{scope:X,decision:D}] → task: { X: "D" }` |
 | `inference.temperature` | `temperature:` | verbatim |
-| `inference.model` = null | *omit `model:`* | null ⇒ OpenCode default |
-| `inference.model` set | `model:` | verbatim |
+| `inference.tier` | target model selection | resolve through project/adapter tier configuration; if no mapping exists, omit and use the tool default |
 | `inference.maxSteps` | sidecar (advisory) | no native field |
 | `context[]` | `@`-ref / `paths.json` + sidecar `context:<id>` dep | referenced, not inlined |
 | `dependencies[]` | sidecar `dependencies[]` | verbatim |
@@ -233,9 +222,10 @@ bash: [ {scope:"*",decision:deny},                      permission:
                                                              "bash …/router.sh status*": "allow"
 ```
 
-Capability→key map: `bash→bash`, `edit→edit`, `write→write`, `delegate→task`. `read`/`grep`/
-`glob` are granted by default in OpenCode; emit a key only when a rule is not a plain
-`*: allow`. Scalar sugar round-trips as a single `"*"` entry. `ask` is preserved natively —
+Capability→key map: `bash→bash`, `edit→edit`, `write→write`, `delegate→task`. Emit every explicit
+rule, including a plain `*: allow`; omission is reserved for an absent capability. This keeps
+explicit author intent distinct from the tool default and makes parse→serialize round trips exact.
+Scalar sugar round-trips as a single `"*"` entry. `ask` is preserved natively —
 **OpenCode is the only target that can express it.**
 
 **Ordering preservation — three caveats, in decreasing severity:**
@@ -349,8 +339,7 @@ Temperature, delegation, skills, hooks, external context: **full fidelity**.
 | `capabilities` | `tools:` / `disallowedTools:` | **ordered list → coarse allowlist** (§2.3) — lossy |
 | `capabilities.delegate` | `Task` in `tools:` | collapse per §2.3 |
 | `inference.temperature` | **dropped** | warn |
-| `inference.model` = null | omit | harness default |
-| `inference.model` set | `model:` | CC alias (`sonnet`/`opus`/`haiku`) |
+| `inference.tier` | `model:` or omit | resolve through project/adapter tier configuration; omit when no mapping is configured |
 | `inference.maxSteps` | dropped | warn |
 | `context[]` | copied → `context/` + SessionStart hook | **bundled**, not referenced (§2.4) |
 | `dependencies[]` | resolved & bundled | `subagent:` deps force-emit that agent file |
@@ -632,8 +621,7 @@ under `# Agent N: <name>`, union of tools, **max** temperature, first non-null m
 | `capabilities` | prose "# Tool Access" list | **default-scope decision only**; all scoping dropped (§3.3) |
 | `capabilities.delegate` | dropped | `task` unsupported (`ToolMapper` marks it so) |
 | `inference.temperature` | `temperature:` | verbatim; limited range |
-| `inference.model` = null | omit | Cursor default |
-| `inference.model` set | `model:` | `mapOACModelToCursor` (falls back to `gpt-4`) |
+| `inference.tier` | omit + warn | Cursor is experimental in v1; do not invent a concrete model mapping |
 | `context[]` | **inlined** into rules body | copied as prose; MVI comment travels inline verbatim |
 | `dependencies[]` | dropped | no dependency system |
 | `examples[]` | prose | no native support |
@@ -714,8 +702,7 @@ JSON (Open Question #Q9).
 | `capabilities` | `tools` + `permissions` (binary) | **default-scope decision → boolean**; scoping dropped (§4.3) |
 | `capabilities.delegate` | `delegate` tool | partial (`task→delegate`) |
 | `inference.temperature` | `creativity` | `≤0.4→low`, `≤0.8→medium`, `>0.8→high` |
-| `inference.model` = null | omit | Windsurf default |
-| `inference.model` set | `model` | `mapOACModelToWindsurf` (fallback `claude-4-sonnet`) |
+| `inference.tier` | omit + warn | Windsurf is outside v1 until its live format is verified |
 | `context[]` | `contexts[]` path refs | referenced under `.windsurf/context/` |
 | `context[].priority` | `priority` | `critical/high→high`, `medium/low→low` (4→2) |
 | `dependencies[]` | partial | subagent deps → separate JSONs; others dropped |
@@ -787,7 +774,7 @@ Extends `CAPABILITY_MATRIX` in `core/CapabilityMatrix.ts`. Cells: **full** / **p
 | granularPermissions (permissions) | full | none | none | none | only oac has per-scope allow/deny/ask |
 | **scopedPermissions** (permissions) † | full | none | none | none | **NEW** — glob/argument scoping *within* a capability (`**/*.env*`, `router.sh …*`). CC `tools:` is tool-NAME-only (verified, `sub-agents.md`); `Bash(x:*)` is settings.json-only and **plugins cannot ship permissions** (§2.6) |
 | **askTriState** (permissions) † | full | none | none | none | **NEW** — CC *platform* has a settings.json `ask` array, but it is **unreachable from a plugin** (§2.6) ⇒ adapter-reachable support is none; rounds to deny |
-| **permissionRuleOrdering** (permissions) † | full | none | none | none | **NEW** — ordered match-wins lists. oac preserves by convention, not by YAML spec (§1.3). ⚠️ direction unresolved (§0.5) |
+| **permissionRuleOrdering** (permissions) † | full | none | none | none | **NEW** — ordered last-match-wins lists, confirmed by `10`; OpenCode preserves textual insertion order (§1.3) |
 | pathPatterns (permissions) | full | none | none | **none** ✏️ | ✏️ windsurf downgraded partial→none: `Record<string,boolean>` cannot hold a path pattern |
 | taskDelegation (tools) | full | **partial** ✏️ | none | partial | ✏️ claude downgraded full→partial: `Task` is coarse — a scoped delegate allowlist cannot be expressed (§2.8) |
 | bashExecution (tools) | full | full | full | full | name remap only |
@@ -797,7 +784,7 @@ Extends `CAPABILITY_MATRIX` in `core/CapabilityMatrix.ts`. Cells: **full** / **p
 | contextPriority (context) | full | none | none | partial | windsurf 4→2; claude drops; oac sidecar-only. ⚠️ all depend on the MVI parser (§0.6) |
 | contextSubdirs (context) | full | full | none | full | cursor single-file |
 | skillsSystem (context) | full | full | none | partial | claude dir-per-skill; windsurf → context refs; cursor inline |
-| modelSelection (model) | full | full | full | full | **null ⇒ tool default** (locked #2) |
+| inferenceTier (model) | full | partial | none | none | semantic tier only; concrete model mapping is project/adapter config, never canonical content |
 | temperatureControl (model) | full | none | partial | partial | claude drops; cursor limited; windsurf → creativity |
 | maxSteps (model) | full | none | none | none | oac sidecar advisory |
 | hooks — plugin/project-level (advanced) | full | full | none | none | claude: `hooks.json` + `session-start.sh` |
@@ -831,63 +818,21 @@ the current file**.
 
 ---
 
-## Open Questions
-
-1. **Q1 — first-match-wins vs last-match-wins (BLOCKING; §0.5).** The locked shape says
-   first-match-wins; all 34 authored agents and `docs/archive/planning/12-MASTER-SYNTHESIS.md:442`
-   say last-match-wins, and first-match-wins would silently break `coder-agent`'s `router.sh`
-   allowlist and neuter `openagent`'s `sudo *: deny`. The index's own `coder-agent` example is
-   broken as printed under first-match-wins. **Recommend adopting last-match-wins.** This
-   changes generated output for every target and must be resolved before `02` or `03` freeze.
-
-2. **Q2 — implicit default + IR validations (§0.4, §1.3).** For `02`: (a) when a capability has
-   no `*` rule, is the implicit default allow or deny? `coder-agent.edit` (denies only) implies
-   allow; `code-reviewer.delegate` (allows only) implies deny — proposed rule: *"opposite of
-   the decisions present; mixed-without-`*` is an error."* (b) Should the IR reject
-   integer-like scopes and enforce scope-uniqueness per capability? Both are required for
-   OpenCode's map form to round-trip deterministically.
-
-3. **Q3 — are dropped security globs a warning or a blocker? (§2.3)** `coder-agent` on CC gets
-   unrestricted `Edit` with `**/*.env*` / `**/*.key` protections silently gone, and no
-   adapter-side remedy exists (§2.6). Warning (current spec) or hard blocker requiring explicit
-   opt-in?
-
-4. **Q4 — CC agent `tools:` vs `settings.json` interaction (confidence: medium).** If `Bash` is
-   omitted from an agent's `tools:`, can a project `settings.json` `Bash(...)` allow re-enable
-   it for that agent? Docs say subagents have "independent permissions" but don't specify the
-   interaction. §2.6's conclusion holds either way (plugins can't ship permissions), but this
-   determines whether the documented manual remedy needs a `tools:` edit too.
-
-5. **Q5 — skill execution metadata.** CC skills carry `context: fork` + `agent: <id>`. Does the
-   neutral skill IR gain `execution: { isolate?, agent? }`? Where does it degrade on
-   OpenCode/Cursor/Windsurf?
-
-6. **Q6 — plugin `settings.json: {"model": "opusplan"}` is likely a no-op (§2.5).** Docs say
-   plugin settings.json supports only `agent` and `subagentStatusLine`. If confirmed, #264 /
-   commit `40dd267` shipped a setting CC ignores — worth an issue, and it moots v1's Q4.
-   Needs runtime verification.
-
-7. **Q7 — `code-reviewer` warning count: 1 or 2? (§2.8).** Under Option A, `delegate:
-   {contextscout: allow}` is a scoped rule; collapsing it to "no `Task`" is a real loss that
-   §0.3 says must warn → 2 warnings, not the index's 1. The emitted file is identical either
-   way. **Recommend accepting 2 and correcting the index**; the alternative is a silent drop.
-
-8. **Q8 — Cursor: legacy `.cursorrules` vs `.cursor/rules/*.mdc`.** The stub emits legacy
-   single-file. Modern `.mdc` supports multiple rule files (relaxing the merge, changing
-   `multipleAgents: cursor` from `none`) and a `globs:` key — can `globs:` carry *any* of the
-   scoped-permission intent, or does it only scope rule *activation*? Also: do neutral commands
-   become inlined docs or get skipped?
-
-9. **Q9 — Windsurf real format (unverified).** The `.windsurf/config.json` +
-   `.windsurf/agents/*.json` layout is inferred from the stub, never checked against a live
-   install. Confirm: (a) canonical layout vs `.windsurfrules` / `.windsurf/rules/*.md`;
-   (b) any manifest listing all agents; (c) any home for commands; (d) exact `creativity`
-   vocabulary (`low/medium/high` vs numeric); (e) whether *any* scoped permission construct
-   exists.
-
-10. **Q10 — `externalContext: cursor` cell semantics.** Keep `none` (referencing unsupported;
-    inlining captured by the new `contextBundling` row), or reclassify? Needs a decision so
-    `CapabilityMatrix.ts` and this doc agree.
+## Stage 1 question dispositions
+
+1. **Q1 precedence:** last-match-wins, confirmed by `10-PRECEDENCE-EXPERIMENT.md`.
+2. **Q2 implicit default and scope validation:** ratified in `02` §§1.2.5–1.2.6.
+3. **Q3 security-glob loss:** build blocker with explicit `--allow-unsafe-degradation` opt-in.
+4. **Q4 CC `tools:` interaction:** accepted uncertainty; fail-closed output omits `Bash`, and
+   `oac doctor --verify` tests the loaded result before release.
+5. **Q5 skill execution metadata:** represented by the neutral skill execution/target fields in
+   `02`; unsupported targets report degradation.
+6. **Q6 plugin `settings.json` model:** accepted as non-authoritative and omitted from v1 output.
+7. **Q7 warning count:** two warnings for `code-reviewer`, four for `coder-agent`; `05` and `07`
+   assert the exact counts.
+8. **Q8 Cursor format:** experimental, not a first-class v1 target; no fidelity promise.
+9. **Q9 Windsurf format:** cut from v1 until verified against a live install.
+10. **Q10 Cursor cell semantics:** `none` for v1; loss is explicit, never silently upgraded.
 
 **Closed in v2:** v1's Q1 (ClaudeAdapter wrong layout — **confirmed**, now index v2 #4 and
 §2 header) · v1's Q9 (OpenCode safety-pattern preset — **moot**: Option A puts safety globs in

+ 55 - 55
docs/architecture/canonical-refactor/04-cli-build-distribution.md

@@ -41,7 +41,7 @@ and `update.sh` (344 lines). Both bash scripts are **deleted** per the locked de
    and adding OpenCode as just another target (§1, §2).
 3. **Runtime must be Node-portable.** `bin/oac.js` currently `execFileSync('bun', …)` and the
    whole CLI uses Bun-only APIs (`Bun.file`, `Bun.write`, `import.meta.dir`). A user running
-   `npm i -g @nextsystems/oac` will **not** have Bun. This is the single biggest cross-platform
+    `npm i -g @controlstack/oac` will **not** have Bun. This is the single biggest cross-platform
    install blocker and is resolved in §4.1.
 
 ---
@@ -204,7 +204,8 @@ Select the adapter from `AdapterRegistry` for the target and call `adapter.fromO
 agent (and the analogous skill/command transforms per `03-adapter-specs.md`). The adapter returns a
 `ConversionResult` with `configs[]` (path + content) and `warnings[]`. `CapabilityMatrix` predicts
 lossy transforms (temperature dropped for Claude, granular permissions flattened to a tool allowlist,
-`model: null` → omit line so the tool default applies — **no hardcoded models**, locked decision #2).
+semantic tier resolved by project/adapter configuration or omitted for the tool default — **no
+hardcoded models**, locked decision #2).
 All warnings surface to the user; `--verbose` lists each, otherwise a count.
 
 ### Stage 5 — Bundle context
@@ -218,14 +219,19 @@ Context files referenced by an agent's `context[]` are copied/wired per target:
   warns at 80KB / errors at 100KB for Cursor — that guard carries into `build`).
 
 ### Stage 6 — Write
-Write each `config.path` under the target's output root (or `--out`). Writes go through the existing
-**collision/update engine** (`installer.ts` decide-logic): a generated file whose on-disk sha256
-still matches the manifest is safely overwritten; a user-modified generated file is skipped unless
-`--yolo` (then backed up to `.oac/backups/…`). This is how "no accidental overwrite" (#321, #326)
-is enforced for generated output, not just copied source. `--dry-run` prints a unified diff instead.
+`oac build` is a pure projection of canonical content and build configuration. It writes every
+`config.path` under the target output root (or `--out`) through a temp directory followed by an
+atomic replace. Existing generated files are replaced even when manually edited; generated output
+is not an authoring surface. `--check` builds to a temp directory and fails on any diff, while
+`--dry-run` prints that diff without writing.
+
+The collision/update engine applies only when `init`, `add`, or `update` changes user-owned
+canonical `content/` or custom files. Those commands skip user-modified source unless `--yolo`
+(backup then overwrite). Keeping source-update safety outside `build` preserves both user edits and
+the invariant that identical content/config always produces identical generated output.
 
 ### Stage 7 — Manifest generation
-Every written file gets a manifest entry: `{ sha256, type, source: "generated", target, installedAt }`.
+Every written file gets a deterministic manifest entry: `{ sha256, type, source: "generated", target }`.
 The manifest becomes the single source of truth for "what OAC produced" — replacing `install.sh`'s
 ad-hoc collision scan and `update.sh`'s find-everything approach. `source` distinguishes
 `bundled` (neutral `/content` copied), `registry` (added component), `generated` (build output),
@@ -240,9 +246,10 @@ Cursor file ≤ size limit). Validation failures in `--strict`/`--check` → exi
 
 ### 2.1 Determinism requirements
 - Stable sort of inputs (by id) before writing.
-- No timestamps **inside** generated content (timestamps live only in the manifest).
+- No timestamps inside generated content or the deterministic manifest. Optional operational
+  timestamps live in separate state/telemetry and are ignored by build/check comparisons.
 - Frontmatter key order fixed by the adapter, not by object-iteration order.
-- `oac build && oac build` is a no-op (second run: all sha256 match → 0 changes).
+- `oac build && oac build` is a no-op because both runs generate and compare the same temp tree.
 
 ### 2.2 Profiles
 Profiles (`essential`, `developer`, `business`, `full`, `advanced`) live in `registry.json`
@@ -267,6 +274,22 @@ survive as a **Stage-5 concern**: when the OpenCode target root is not the defau
 `.opencode/` (i.e. `--global` or `--dir`), the adapter/bundler rewrites context path references to
 the resolved absolute location. This is central to issues #321/#326 (global vs project-local paths).
 
+### 2.5 Project discovery (`.oac.json`)
+
+Use one discovery chain:
+
+1. If `<cwd>/.oac.json` exists, parse `context.root`; resolve it relative to that file and require
+   the directory plus its registry/root anchor. Invalid config is a hard error, not a silent scan.
+2. Otherwise walk parent directories to the filesystem root looking for `.oac.json`.
+3. If none exists, accept `<project>/content/` only when its registry/root anchor validates it.
+4. After successful fallback discovery in a writable project, write `.oac.json` atomically with
+   `{ "version": 1, "context": { "root": "content" } }` so the next run takes the fast path.
+5. If discovery fails, stop with `oac init` guidance; never fall back to `.opencode/context` or a
+   global directory implicitly.
+
+`OAC_PACKAGE_ROOT` is separate: it locates OAC's bundled starter content, not the user's editable
+project content. All commands call the same typed discovery function from `packages/core`.
+
 ---
 
 ## 3. `install.sh` → CLI parity checklist (all 35 functions)
@@ -364,8 +387,9 @@ must run on stock Node**. Root `package.json` already declares `engines.node >=1
 
 - Default is **project-local** — the safe, no-surprise default. `--global`/`--dir` are explicit
   opt-ins (mirrors `install.sh`'s location menu but flag-driven).
-- **No accidental overwrite (#321/#326):** every write is gated by the sha256 manifest — a file
-  the user edited is never silently replaced. Path normalization (`lib/paths.ts`) rejects paths that
+- **No accidental source overwrite (#321/#326):** `init`/`add`/`update` writes to canonical or
+  custom files are gated by the sha256 manifest; generated targets are replaced by pure `build`.
+  Path normalization (`lib/paths.ts`) rejects paths that
   escape the install root (`update.sh` already guarded `../` — preserve that: reject `..`/absolute
   escapes before any write).
 - Global path today is `~/.config/opencode` on all platforms (from `get_global_install_path`).
@@ -388,21 +412,19 @@ must run on stock Node**. Root `package.json` already declares `engines.node >=1
 
 | Method | Command | Use case | Notes |
 |--------|---------|----------|-------|
-| **npx (no install)** | `npx @nextsystems/oac init` | one-off / trial / CI | Downloads on demand; needs Node ≥18; the modern replacement for `curl \| bash`. |
-| **Global** | `npm i -g @nextsystems/oac && oac init` | frequent users | `oac` on PATH; `oac doctor` checks for updates vs npm registry (already implemented). |
-| **Dev/local** | `bun run src/index.ts` or `npm link` | contributors | `OAC_PACKAGE_ROOT` env override lets the CLI find bundled `/content` in the monorepo. |
+| **npx (no install)** | `npx @controlstack/oac init` | one-off / trial / CI | Downloads on demand; needs Node ≥20; the modern replacement for `curl \| bash`. |
+| **Global** | `npm i -g @controlstack/oac && oac init` | frequent users | `oac` on PATH; `oac doctor` checks for updates vs npm registry. |
+| **Dev/local** | `pnpm --dir packages/cli dev` | contributors | `OAC_PACKAGE_ROOT` lets the CLI find bundled `/content` in the monorepo. |
 
 Registry is **bundled**, so `npx` and global both work **offline after download** — no network at
 run time (unlike `install.sh`, which curled every file from `raw.githubusercontent.com`).
 
 ### 4.5 Registry fetching, caching, offline
 
-- **Default: bundled + offline.** `registry.json` and `/content` ship inside the package
-  (`readRegistry` reads from `getPackageRoot()`). No network needed for `init/add/build/update`.
-- **Optional remote registry** (parity with `install.sh` `REGISTRY_URL`/`OPENCODE_BRANCH`): a
-  `--registry <url>` flag or `OAC_REGISTRY_URL` env can fetch a newer catalog via `fetch`. Cache it
-  under `~/.cache/oac/registry-<hash>.json` (or `%LOCALAPPDATA%` on Windows) with a TTL; fall back to
-  the bundled copy when offline. This subsumes `fetch_registry`'s `file://`/remote branching.
+- **Bundled + offline only in v1.** `registry.json` and `/content` ship inside the package
+  (`readRegistry` reads from `getPackageRoot()`). No network is needed for `init/add/build/update`.
+  A remote catalog without matching bundled source cannot install a component reliably, so v1 has
+  no `--registry` flag or registry cache.
 - **`doctor`** already does a non-blocking npm-latest check with a 5s timeout and offline fallback —
   the model for all network calls: **never block on the network**.
 
@@ -427,7 +449,7 @@ manifest**. Migration path (details owned by Agent E; CLI responsibilities here)
 3. Because the pre-refactor repo had `.opencode/` as *source*, migration also seeds `content/` from
    the bundled neutral source so future `oac build` works — offering a diff if the user hand-edited
    `.opencode/` agents.
-4. README swaps `curl … | bash` for `npx @nextsystems/oac init`; the old one-liner can print a
+4. README swaps `curl … | bash` for `npx @controlstack/oac init`; the old one-liner can print a
    deprecation notice pointing at npm (kept for one release).
 
 ---
@@ -471,43 +493,21 @@ dev/monorepo override. This is why the CLI works offline. The only change is the
 
 ### 5.3 Versioning
 
-- **Single version** for the whole product = root `package.json` `version` (currently `0.7.1`),
+- **Single version** for the whole product = root `package.json` `version`,
   surfaced by `readCliVersion()` and written into `.oac/manifest.json` `oacVersion` on init/update.
   `doctor` compares it to the npm-latest.
-- The `@nextsystems/oac-cli` sub-package version should track the root (or be dropped in favor of a
-  single published package — Open Question). `bump-version.sh` + `npm version` scripts already exist
-  and write `VERSION`; keep them, but ensure `/content`, adapters, and CLI ship as one coherent
-  version so a manifest's `oacVersion` unambiguously identifies the source that produced the output.
+- Publish exactly one package, `@controlstack/oac`; core and adapters are private and bundled into
+  its CLI output. Legacy package names remain installable only for rollback and are deprecated with
+  a pointer to the canonical package. `/content`, adapters, and CLI ship as one coherent version so
+  a manifest's `oacVersion` identifies the source that produced the output.
 - **Manifest schema version** (`manifest.version: "1"`) and **registry schema version**
   (`registry.schema_version`) are independent of the product version and gate migrations.
 
 ---
 
-## Open Questions
-
-1. **Runtime choice (§4.1):** ship a Node-portable build (option A, recommended) or per-platform
-   compiled binaries (option B)? This determines the whole packaging story and must be decided first.
-2. **Single vs. multi package:** publish one `@nextsystems/oac` (CLI + core + adapters + content) or
-   keep `@nextsystems/oac-cli` + `@openagents-control/compatibility-layer` separate? Affects `bin`,
-   `files`, and version coupling (§5.3).
-3. **Interactive prompts:** `install.sh` had rich menus. Do we ship any interactivity (e.g.
-   `@clack/prompts` for `oac init` when TTY), or stay strictly flag-driven + non-interactive? The
-   parity table treats menus as obsolete, but some users liked the guided flow.
-4. **Default target for `oac init`:** when no IDE is detected and no `--target` given, default to
-   `opencode` (assumed here) or prompt/refuse? Ties to Q3.
-5. **Global path on Windows (§4.2):** keep `~/.config/opencode` everywhere (current bash behavior),
-   or use `%APPDATA%`/`%LOCALAPPDATA%` on Windows? The latter is more native but diverges from
-   existing installs.
-6. **Remote registry (§4.5):** is a fetchable/newer-than-bundled registry actually needed, given
-   `/content` is bundled and versioned? If components can't be installed without their bundled
-   source, a remote registry pointing at unbundled components is meaningless — likely drop it and
-   keep registry fully bundled.
-7. **`apply` deprecation window:** how long is `oac apply` kept as an alias for
-   `oac build --target`? (Coordinate with Agent E's migration timeline.)
-8. **Backup retention:** `.oac/backups/` grows unbounded (never auto-pruned, unlike `update.sh`'s
-   trap cleanup). Add a retention policy / `oac clean` command, or leave to the existing cleanup skill?
-9. **`content/` on disk vs. build-only:** does the user keep an editable `content/` in their project
-   (source they can customize, then `oac build`), or is `/content` purely internal to the package and
-   only targets land in the project? This is the biggest UX fork and affects `init`, `add`, `update`,
-   and migration (§4.7). Needs alignment with Agent A/B.
-```
+## Stage 1 decisions
+
+`08-STRUCTURE-AND-PACKAGING.md` closes the former questions: Node-portable CLI; one published
+`@controlstack/oac` package; flags-first prompts with TTY guidance; `agents-md` as the non-interactive
+fallback target; native per-OS config paths; bundled/offline registry; `apply` retained for one minor
+release; 30-day backup retention through `oac clean`; and editable project `content/`.

+ 54 - 0
docs/architecture/canonical-refactor/06-REVIEW.md

@@ -999,3 +999,57 @@ The failure mode here is not sloppiness. It is that **five authors revised at di
 against a moving index, and the index recorded its conclusions without telling the documents that
 supplied them.** Fixes #1–#4 are all versions of the same repair: make the documents agree with
 each other and with the disk. Do that and this is buildable.
+
+---
+
+## Stage 1 disposition table
+
+> **Gate closure:** 2026-07-15, canonical-refactor subtask 10.
+> **Rule:** `fixed` means the governing specification now contains the required decision or
+> correction. `accepted` means the limitation is intentionally outside the first-class v1 scope
+> and has an explicit guardrail. These dispositions override the original review text where they
+> conflict, per `12-DISPATCH.md`.
+
+| Finding | Disposition | Resolution evidence |
+|---|---|---|
+| **F1** | **fixed** | `05-impact-migration-tests.md` v2 replaces the false 160-agent scope with the disk-verified inventory and merge stages. |
+| **F2** | **fixed** | `02-canonical-schema.md` v3 §0.2 unifies all dependency kinds and accepts path/wildcard refs; Stage 2's corpus gate covers the 19 former rejects. |
+| **F3** | **fixed** | `10-PRECEDENCE-EXPERIMENT.md` confirms last-match-wins through live OpenCode 1.17.20 probes and resolver `.findLast()` evidence. |
+| **F4** | **fixed** | `00-INDEX.md` v2.2 and `02-canonical-schema.md` §4 use the corrected leading-window priority distribution. |
+| **F5** | **fixed** | `01-feature-inventory.md` v2 uses exact-basename navigation counts and preserves the separate suffix-named files. |
+| **F6** | **fixed** | `01-feature-inventory.md` v2 retracts the committed-secret claim; `.env` remains an untracked build-hygiene exclusion only. |
+| **F7** | **fixed** | `00-INDEX.md` v2.2, `01` §4.5, `02` §2, `03` §0.1, and `09` §4 align on 12 CC skills plus 4 canonical OpenCode skills. |
+| **F8** | **fixed** | `00-INDEX.md` v2.2 and `02`'s `inference.tier` preserve the five-sonnet/two-haiku semantic cost tiers without authoring model ids. |
+| **F9** | **fixed** | `01-feature-inventory.md` v2 and `09-MERGE-RULES.md` §§7,9 distinguish regular files, symlinks, and JSON; later work must recount after tree changes. |
+| **F10** | **fixed** | `01-feature-inventory.md` v2 corrects the unregistered-context result and separates path aliases from canonical files. |
+| **C1** | **fixed** | `03-adapter-specs.md` §0.5 and its question dispositions now cite `10-PRECEDENCE-EXPERIMENT.md`; stale precedence blockers are removed. |
+| **C2** | **fixed** | `05-impact-migration-tests.md` v2 defines a two-tree merge; `09-MERGE-RULES.md` owns the per-content-type decisions. |
+| **C3** | **fixed** | `02-canonical-schema.md` v3 makes raw model ids non-authorable and uses `inference.tier`; `05` tests the same contract. |
+| **C4** | **fixed** | `05-impact-migration-tests.md` and `07-EXECUTION-PLAN.md` assert exact CC warning counts: two for `code-reviewer`, four for `coder-agent`. |
+| **C5** | **fixed** | `08-STRUCTURE-AND-PACKAGING.md` §§2.1–2.2 distinguishes bundled canonical content from each user's editable project `content/`. |
+| **C6** | **fixed** | `01-feature-inventory.md` v2 aligns capability examples to `{scope, decision}`, matching `02`. |
+| **C7** | **fixed** | The corrected skill census is aligned across `00`, `01`, `02`, `03` §0.1, and `09` §4. |
+| **C8** | **fixed** | `02-canonical-schema.md` §1.2.5 owns the implicit-default rule; `03` §0.4 consumes it without escalation. |
+| **C9** | **accepted** | `07-EXECUTION-PLAN.md` limits first-class v1 targets to OpenCode and Claude; Cursor is experimental and Windsurf is cut, so unsupported hooks/delegation remain explicit blockers outside the v1 promise. |
+| **C10** | **fixed** | `03-adapter-specs.md` corrects the archived citation to line 432; live behavior is now sourced from `10`, not that citation. |
+| **L1** | **fixed** | `09-MERGE-RULES.md` is the signed-off owner and conflict contract for every content type. |
+| **L2** | **fixed** | `02-canonical-schema.md` v3 adds `targets:[]`; `09` uses it for the 16-skill union and target-specific hooks. |
+| **L3** | **fixed** | `09-MERGE-RULES.md` §§3,6 tokenizes path-bearing bodies and permission scopes; `07` Stage 4 adds a no-raw-`.opencode/` lint. |
+| **L4** | **fixed** | `02-canonical-schema.md` v3 adds `inference.tier: fast|balanced|deep`, retaining per-agent intent without hardcoded models. |
+| **L5** | **fixed** | `02-canonical-schema.md` v3 restores profile objects including `additionalPaths`, badges, descriptions, and components. |
+| **L6** | **fixed** | `02-canonical-schema.md` v3 models registry categories and the promoted `icon`/`order`/`status` metadata. |
+| **L7** | **fixed** | `02-canonical-schema.md` v3 models aliases; `09` §7 defines symlink and duplicate-id collapse into `aliases[]`. |
+| **L8** | **fixed** | `09-MERGE-RULES.md` §§3,7 resolves delegate names through canonical ids or aliases, preserving mixed-case legacy scopes such as `TestEngineer`. |
+| **L9** | **fixed** | `02-canonical-schema.md`'s MVI serializer specifies SemVer normalization with compact `X.Y` re-emission; `05` requires byte-stable round trips. |
+| **L10** | **fixed** | `08-STRUCTURE-AND-PACKAGING.md` §2 defines `.oac.json` as project discovery for editable `content/`; `04` owns CLI discovery/self-healing behavior. |
+| **G1** | **fixed** | `02` resolves dependency vocabulary/defaults, `09` resolves aliases/path scopes, and `07` Stage 3 makes security loss a blocker with explicit opt-in. |
+| **G2** | **fixed** | `02-canonical-schema.md` v3 defines path-derived context identity, optional metadata, split taxonomy, leading-window MVI parsing, and stable re-emission. |
+| **G3** | **fixed** | `02` restores profiles/categories/aliases and plural registry sections; `04` §2.3 defines dependency and section-key normalization. |
+| **G4** | **fixed** | `08-STRUCTURE-AND-PACKAGING.md` §4 replaces both brittle anchors with an explicit package-name check plus `OAC_PACKAGE_ROOT`. |
+| **G5** | **fixed** | `02` §1.2.5 distinguishes absent/default capabilities from explicit `*: allow`; `05` requires OpenCode parse→build idempotence. |
+| **G6** | **fixed** | `04-cli-build-distribution.md` separates deterministic build/check output from update-time sha256 user-edit protection; `07` Stage 5 tests preservation and `--yolo`. |
+| **G7** | **fixed** | `05-impact-migration-tests.md` v2 removes the `diff-explained` escape hatch; `07` Stage 3 requires reviewed, byte-stable goldens and explicit merge decisions. |
+
+All **37** F/C/L/G findings are dispositioned above: **36 fixed, 1 accepted**. U2 is closed by
+`11-ROLLBACK.md`; U1 and U3 are closed by `09-MERGE-RULES.md`; remaining U-items are carried as
+explicit implementation/test constraints in `02`, `05`, and `07` rather than open Stage 1 gates.

+ 7 - 7
docs/architecture/canonical-refactor/07-EXECUTION-PLAN.md

@@ -23,7 +23,7 @@ loads in each real tool. Never degrade silently.
 | Language | **TypeScript everywhere** — kill the 52KB `install.sh`/`update.sh` and the Bun-only APIs | One codebase, testable, cross-platform; bash installers are the root of the Windows bug class (#304, #312) |
 | Runtime | **Node ≥ 20** (no Bun requirement) | `npm i -g` must work on a clean machine; Bun coupling defeats npm distribution (15 files to convert, mechanical) |
 | Workspace manager | **pnpm** (workspaces + lockfile) | Security: strict non-flat `node_modules` (no phantom deps), `ignore-scripts` by default, single content-addressed store, better lockfile integrity. Replaces the current bun.lock + package-lock.json split |
-| Distribution | **npm registry** (`npm i -g oac` / `npx oac`) | Users install with the tool they already have; pnpm is our dev tool, not a user requirement |
+| Distribution | **npm registry** (`npm i -g @controlstack/oac` / `npx @controlstack/oac`) | Users install with the tool they already have; the installed binary remains `oac`; pnpm is our dev tool, not a user requirement |
 | First-class targets v1 | **OpenCode + Claude Code** (+ cheap `agents-md` target) | The two with real users and verified formats. Windsurf CUT from v1 (format never verified against a live install); Cursor = experimental |
 | Core product | **Context management** — team standards versioned, installed, updated safely | This is the wedge: "bring your standards, we deploy them to every tool." The generic context library becomes optional starter profiles |
 | Verification | **Install-verification matrix** as a product feature (`oac doctor --verify`) and a CI gate | "It installed" ≠ "the tool loads it." Headless real-tool load checks across OS × tool × profile |
@@ -84,7 +84,7 @@ the refactor — it needs a week.
 ## Stage 1 — Spec repair & decisions (docs only, no code)
 
 **Why:** `06-REVIEW`'s verdict is verified: the centerpiece schema rejects the corpus it
-describes (19 real dependency refs, all 294 context files), two docs were never revised
+describes (19 real dependency refs and the full context corpus recorded in `09` §9), two docs were never revised
 after v2 decisions, and the hardest migration task (the merge) has no owner. Writing code
 against a self-contradicting spec reproduces the drift we're curing.
 
@@ -117,7 +117,7 @@ against a self-contradicting spec reproduces the drift we're curing.
 - Zero open questions labelled BLOCKING across `01`–`05` (each of `06`'s F/C/L/G findings
   marked *fixed* or *accepted* in a disposition table appended to `06`).
 - The precedence experiment's transcript committed to this directory.
-- Merge conflict rules signed off (a table in `08-MERGE-RULES.md`: content type → source of
+- Merge conflict rules signed off (the table in `09-MERGE-RULES.md`: content type → source of
   truth → conflict resolution).
 
 ---
@@ -141,8 +141,8 @@ implementation. This is the forcing function that proves Stage 1 actually worked
    resolution; parse-time validation for integer-like scopes and duplicate scopes.
 
 **Tests / gate (all in CI from this stage on):**
-- **Corpus test (Layer 1):** every real file parses — 34 agents, 294 context files, 20
-  commands, 16 skills, `registry.json` including all 19 previously-rejected dependency refs
+- **Corpus test (Layer 1):** every real file parses — 34 agents, the context corpus recorded in
+  `09` §9, 20 commands, 16 skills, `registry.json` including all 19 previously-rejected dependency refs
   and all 5 profiles. Zero failures, zero skips.
 - **Trap tests:** the line-232/line-301 prose-marker files parse with correct (path-derived)
   metadata, NOT the in-body marker; the 3 dual-format files resolve MVI-wins; the
@@ -194,7 +194,7 @@ sets; `session-start.sh` exists only on the CC side). It is executed here — ag
 written Stage-1 rules — not improvised under deadline.
 
 **Core features:**
-1. Seed `/content/` from **both** trees per `08-MERGE-RULES.md`; harvest CC `<example>`
+1. Seed `/content/` from **both** trees per `09-MERGE-RULES.md`; harvest CC `<example>`
    blocks, the 12 CC skills, 6 CC-only commands, and `session-start.sh`'s six capabilities.
 2. **Path tokenization** in bodies *and permission scopes* (`{{SKILL_ROOT}}` etc.) + a lint
    forbidding raw `.opencode/` paths in `/content/` (~129 files affected).
@@ -216,7 +216,7 @@ written Stage-1 rules — not improvised under deadline.
 
 ## Stage 5 — CLI, installer parity & the install-verification matrix
 
-**Why:** This is where the user-facing promise lands: `npm i -g oac` on a clean machine —
+**Why:** This is where the user-facing promise lands: `npm i -g @controlstack/oac` on a clean machine —
 including Windows — with no Bun, no bash, safe updates, and working dependency resolution
 (today `add.ts` installs a component and **none** of its dependencies; issue #310 is a user
 discovering this). And "it installed" must be checkable per tool: your explicit requirement

+ 9 - 5
docs/architecture/canonical-refactor/09-MERGE-RULES.md

@@ -3,6 +3,8 @@
 > **Status:** Decided — written BEFORE Stage 4 executes (per `06-REVIEW.md` recommendation #3
 > and `07-EXECUTION-PLAN.md` Stage 4 feature 1).
 > **Date:** 2026-07-15 · Companion to `07-EXECUTION-PLAN.md` and `08-STRUCTURE-AND-PACKAGING.md`.
+> **Sign-off:** Approved as the Stage 4 merge contract by canonical-refactor subtask 08;
+> reaffirmed at the Stage 1 gate in subtask 10 on 2026-07-15.
 > Every path, count, and line number below was re-verified on disk on 2026-07-15
 > (branch `refactor/canonical`). Where disk disagrees with earlier docs, disk wins and the
 > discrepancy is recorded in §9.
@@ -86,9 +88,10 @@ Grounds, verified:
 | `permission:` (bash/edit/task maps) | → IR rules. Path-bearing scopes (e.g. `"bash .opencode/skills/task-management/router.sh complete*"`, `coder-agent.md:8-9`) are **tokenized** (`{{SKILL_ROOT}}` etc.) per Stage 4 feature 2 — raw `.opencode/` paths are lint-forbidden in `/content/`. |
 | `task:` allowlists (`contextscout`, `externalscout`, `TestEngineer`, …) | → IR delegate rules; referenced agent names resolve via canonical id **or** `aliases[]` (so `TestEngineer` keeps resolving). |
 
-CC-only frontmatter (`tools:` list, `disallowedTools:`, `model:`) maps to the IR fields the
-`02` schema (as amended by 06 fix #1) already defines; no conflict — the OpenCode copies have
-no `model:` at all (verified by grep across both `subagents/code/` and `subagents/core/`).
+CC-only `tools:` and `disallowedTools:` map to capabilities. Legacy `model:` values are migration
+inputs only: the two scout agents become `inference.tier: fast`; the other five become
+`inference.tier: balanced` (or omit the default). Concrete model names are discarded and never
+enter canonical content. OpenCode copies have no `model:` (verified across both subagent trees).
 
 ### 3.3 Mandatory exception — two pairs where OpenCode is longer
 
@@ -104,8 +107,9 @@ structure is the shipped one), **but** for these two pairs the merge is not a bl
 ### 3.4 Identity
 
 Canonical id = CC kebab-case filename (`01` Q1 disposition, `06-REVIEW.md:778`: *filename ==
-id*, `aliases[]` for back-compat). The OpenCode PascalCase `name:` values go into
-`aliases[]` so every existing dependency edge and `task:` permission key keeps resolving.
+id*, `aliases[]` for back-compat). Canonical ids remain kebab-case; the deliberately loose legacy
+alias schema in `02` §0.3 retains OpenCode names such as `TestEngineer`, so existing dependency
+edges and `task:` permission keys keep resolving without making those names canonical.
 
 ---
 

+ 141 - 0
docs/architecture/canonical-refactor/11-ROLLBACK.md

@@ -0,0 +1,141 @@
+# 11 — Release Pinning, Abort Criteria, and Rollback
+
+> **Status:** Decided — Stage 1 gate artifact.
+> **Date:** 2026-07-15 · Applies to Stages 0–6 of `07-EXECUTION-PLAN.md`.
+> **Authority:** This document closes `06-REVIEW.md` U2. It must be exercised by the Stage 6
+> release-candidate drill before any auto-updating canonical release is promoted.
+
+---
+
+## 1. Safety contract
+
+The refactor may advance only while the previous known-good release remains installable. A failed
+gate stops the current stage; it never becomes a reason to weaken, skip, or relabel the gate. Until
+Stage 6 completes its rollback drill, the hand-maintained producers and their release artifacts are
+retained.
+
+Every release records one version across the npm package, marketplace entry, plugin manifest, and
+generated manifest. Release tags are immutable. The rollback target is always an already-verified
+version or immutable repository tag, never a moving branch.
+
+## 2. Claude Code plugin pin and rollback
+
+Claude Code's supported install form is:
+
+```text
+/plugin install oac@oac-marketplace
+```
+
+Here `@oac-marketplace` selects the marketplace; it is **not a version selector**. There is no
+documented `/plugin install oac@1.1.0` downgrade command. OAC therefore pins the marketplace
+repository itself to an immutable Git tag. A full SHA is valid only for an explicitly configured
+plugin source; it is not claimed as marketplace URL syntax here.
+
+### Pin a known-good release
+
+1. In `/plugin` → **Marketplaces**, disable automatic updates for `oac-marketplace`.
+2. Uninstall the currently loaded plugin:
+
+   ```text
+   /plugin uninstall oac@oac-marketplace
+   ```
+
+3. Remove the moving marketplace source in the `/plugin` marketplace UI, then add the known-good
+   repository tag (replace `v1.1.0` with the required release):
+
+   ```text
+   /plugin marketplace add https://github.com/darrenhinde/OpenAgentsControl.git#v1.1.0
+   ```
+
+4. Reinstall and reload:
+
+   ```text
+   /plugin install oac@oac-marketplace
+   /reload-plugins
+   ```
+
+5. Confirm `/plugin` reports the expected marketplace/plugin version before resuming work.
+
+For a fleet-wide emergency freeze, set `DISABLE_AUTOUPDATER=1` in Claude Code's environment. That
+disables global automatic updates and is broader than the per-marketplace control, so remove it
+after the incident is resolved.
+
+### Publisher rollback
+
+The maintainer creates a new patch release from the last known-good full SHA; published tags are
+never moved. The marketplace version and plugin manifest must match that patch version. Users on a
+pinned old tag remain stable, while users following the marketplace receive the corrective patch.
+
+## 3. npm CLI pin and rollback
+
+The decided Stage 6 package is `@controlstack/oac`. The `controlstack` npm organization must be
+claimed before release; failure to claim it aborts Stage 6 rather than silently changing package
+identity.
+
+Install or restore an exact known-good version:
+
+```bash
+npm install --global @controlstack/oac@1.1.0
+oac --version
+npm view @controlstack/oac@1.1.0 version dist.integrity
+```
+
+For a one-off invocation that does not replace a global install:
+
+```bash
+npx --yes @controlstack/oac@1.1.0 doctor --verify
+```
+
+The maintainer never overwrites or unpublishes a known-good version. A regression is corrected by
+publishing a new patch from the last known-good SHA, verifying its provenance/integrity, and moving
+the npm `latest` tag only after the verification matrix passes.
+
+## 4. Abort and revert criteria by stage
+
+| Stage | Abort criterion | Required action before retry |
+|---|---|---|
+| **0 — foundations** | Frozen workspace install, required deterministic package matrix, security hotfix, or one-version check fails. | Do not start new packages. Revert the failing foundation change or document pre-existing non-blocking debt explicitly; restore the prior lockfile/release state if installs are not reproducible. |
+| **1 — specification** | Any `BLOCKING` question remains in `01`–`05`, any F/C/L/G finding lacks a disposition, merge rules are unsigned, or rollback is not executable. | Stop implementation. Reconcile the documents and evidence; no Stage 2 code is written against an ambiguous contract. |
+| **2 — core IR** | The corpus test has any failure or skip, a real dependency ref is unrepresentable, or parse→serialize changes a protected fixture unexpectedly. | Stop downstream work. Fix or revise the IR with an explicit Stage 1 disposition; do not add per-file bypasses. |
+| **3 — adapters/build** | Golden output is unstable, a first-class target loses required security/delegation semantics, warning counts drift unexplained, or unsafe degradation succeeds without explicit opt-in. | Stop and retain the current producers. If the IR cannot express the real agents, fall back to OpenCode-as-source plus documentation generation as defined in `07`. |
+| **4 — content merge** | An unowned conflict appears, the two trees prove to be different products, generated output fails a real-tool load, or eval performance drops below the recorded baseline. | Stop the merge and keep both source trees. Apply `09-MERGE-RULES.md`; if the trees are different products, choose one first-class product instead of forcing a lossy union. |
+| **5 — CLI/parity** | Any supported OS needs Bun/bash, packed install fails, dependency closure is incomplete, user-edited files are overwritten without consent, or `doctor --verify` cannot prove target loading. | Keep the legacy installer available. Revert to the last packed CLI that passes the 3-OS matrix and repair parity before deletion. |
+| **6 — flip/release** | Build is not clean/idempotent, the npm scope is unavailable, canary verification regresses, or either Claude/npm rollback drill cannot restore the prior version. | Do not move `latest`, enable marketplace auto-update, delete legacy producers, or uncommit generated trees. Restore the previous npm dist-tag and pinned marketplace source, then repeat the full drill. |
+
+## 5. Stage 6 release-candidate drill
+
+For candidate `x.y.z-rc` with previous known-good `x.y.(z-1)`:
+
+1. Build once from canonical `content/`; require a clean second build and clean Git diff.
+2. Pack/install the candidate on Ubuntu, macOS, and Windows without Bun; run
+   `oac init` and `oac doctor --verify` for every first-class target.
+3. Publish the candidate under an npm prerelease tag, never `latest`.
+4. Point a canary Claude marketplace source at the candidate tag and disable automatic
+   promotion. Verify plugin discovery, agents, hooks, skills, and bundled context.
+5. Roll the npm canary back with an exact `@controlstack/oac@x.y.(z-1)` install and rerun
+   `doctor --verify`.
+6. Roll the Claude canary back by uninstalling, adding the `vX.Y.(Z-1)` marketplace repository
+   tag, reinstalling, and reloading plugins. Verify the displayed version and a real agent load.
+7. Record commands, versions, SHAs, integrity values, and verification output in the release
+   artifact. Any failed step triggers the Stage 6 abort criterion.
+
+Only after both rollback paths pass may CI promote npm `latest`, update the moving marketplace,
+and permit automatic updates.
+
+## 6. Evidence and limitations
+
+Official Claude Code documentation supports marketplace repository branch/tag `#ref` pinning,
+plugin source `ref`/full SHA, exact npm source versions, and per-marketplace update control. It does not document
+arbitrary user-side plugin version syntax or a direct downgrade command. This procedure therefore
+uses only the supported repository pin and reinstall flow.
+
+References:
+
+- <https://code.claude.com/docs/en/discover-plugins>
+- <https://code.claude.com/docs/en/plugin-marketplaces>
+- <https://code.claude.com/docs/en/plugin-dependencies>
+- <https://code.claude.com/docs/en/plugins-reference>
+- <https://code.claude.com/docs/en/setup#disable-auto-updates>
+- `07-EXECUTION-PLAN.md` Stage 6 and kill criteria
+- `08-STRUCTURE-AND-PACKAGING.md` §§3–5
+- `09-MERGE-RULES.md` (signed-off Stage 4 merge contract)

+ 5 - 5
docs/architecture/canonical-refactor/12-DISPATCH.md

@@ -24,9 +24,9 @@
 | 00–06 | Spec set + adversarial review | ✅ |
 | 07 | Execution plan (stages, gates, kill criteria) | ✅ |
 | 08 | Repo structure, build flow, packaging (`@controlstack/oac`) | ✅ |
-| 09 | MERGE-RULES (created by subtask 08) | ⏳ |
-| 10 | PRECEDENCE-EXPERIMENT transcript (created by subtask 09) |  |
-| 11 | ROLLBACK story (created by subtask 10) |  |
+| 09 | MERGE-RULES (created by subtask 08; signed off by subtask 10) | ✅ |
+| 10 | PRECEDENCE-EXPERIMENT transcript (created by subtask 09) |  |
+| 11 | ROLLBACK story (created by subtask 10) |  |
 | 12 | This dispatch guide | ✅ |
 
 Where a subtask brief and a spec doc conflict, precedence is:
@@ -46,8 +46,8 @@ STOP and report — do not guess.
 - `packages/plugin-abilities` — check its `package.json` scripts before wiring CI; do not assume.
 - `evals/framework` — has NO ESLint config; its deterministic baseline = build + the
   `test:ci` vitest allowlist + `validate:suites:all`. Do not add lint gates to it in Stage 0.
-- Known census discrepancy: 00-INDEX says 296 context `.md`, 07 says 294 files + 3 symlinks.
-  **Recount from disk with the leading-window rule before asserting either.**
+- Context census is recorded from disk in `09-MERGE-RULES.md` §9. Recount before asserting it
+  after any content-tree change; historical numbers in `00`–`07` are not runtime authority.
 - CC plugin agents live in `plugins/claude-code/agents/` (7 agents: 5 `sonnet`, 2 `haiku`).
   `.claude-plugin/marketplace.json` is live production config — touch only when a brief says so.