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`. |
 | `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`). |
 | `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. |
 | `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`. |
 | `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). |
 | `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**. |
 | `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. |
 | `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. |
 | `tools` (allowlist) | `capabilities` allow-set | 🟢 | Derived. |
 | `disallowedTools` | `capabilities` deny-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
 > 🔴 **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/`,
 > 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 |
 | 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` | 🔵 | |
 | Prompt templates | `.opencode/prompts/*/TEMPLATE.md`, `README.md` | 🔵 | |
 | Eval results | `.opencode/prompts/core/openagent/results/*.json`, `default-output.log` | 🔵 | Build artifacts committed to git. |
 | 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). |
 | `.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`
 - [ ] `description` → neutral `description`
 - [ ] `mode: primary|subagent` → neutral `role`
 - [ ] `mode: primary|subagent` → neutral `role`
 - [ ] `temperature` (33 agents) → `inference.temperature`; **Claude drops it → warning**
 - [ ] `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)
 - [ ] `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*
 - [ ] **Glob-scoped denies preserved** (`**/*.env*`, `**/*.key`, `**/*.secret`, `node_modules/**`, `.git/**`) — *security control*
 - [ ] **Deny-by-default + narrow allow preserved** (`coder-agent` bash allowlist) — *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
 - [ ] 🔴 `.context-manifest.json` `profile: "standard"` (not a real profile name) fixed
 
 
 ### Prompts / misc
 ### 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
 - [ ] `prompts/*/TEMPLATE.md` + READMEs preserved
 - [ ] Eval `results/*.json` — keep or gitignore
 - [ ] Eval `results/*.json` — keep or gitignore
 - [ ] `.opencode/docs/` (3 docs) relocated
 - [ ] `.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,
 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.
 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`
 **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
 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.
 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
 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`.
    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
 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.
    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).
 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]+)*$/,
 export const IdSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
   "id must be kebab-case: lowercase alphanumeric words joined by single hyphens");
   "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. */
 /** Human display name. HINT for casing — adapters slugify/re-case freely. */
 export const NameSchema = z.string().min(1);
 export const NameSchema = z.string().min(1);
 
 
@@ -593,9 +599,9 @@ the sole owner — closes v2 Q9.)*
 export const AgentSchema = z.object({
 export const AgentSchema = z.object({
   // ---- identity (folded-in metadata; NO sidecar) ----
   // ---- identity (folded-in metadata; NO sidecar) ----
   id: IdSchema,                              // filename == id (01 Q1, per 06-REVIEW triage)
   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,
   name: NameSchema,
   description: DescriptionSchema,
   description: DescriptionSchema,
   role: RoleSchema,                          // was `mode`
   role: RoleSchema,                          // was `mode`
@@ -668,7 +674,7 @@ export const SkillFileSchema = z.object({
 
 
 export const SkillSchema = z.object({
 export const SkillSchema = z.object({
   id: IdSchema,
   id: IdSchema,
-  aliases: z.array(IdSchema).default([]),    // NEW v3
+  aliases: z.array(AliasSchema).default([]), // NEW v3
   name: NameSchema,
   name: NameSchema,
   description: DescriptionSchema,
   description: DescriptionSchema,
   category: CategorySchema,
   category: CategorySchema,
@@ -707,7 +713,7 @@ Real source: `.opencode/command/add-context.md`, `context.md`. Verified frontmat
 ```ts
 ```ts
 export const CommandSchema = z.object({
 export const CommandSchema = z.object({
   id: IdSchema,
   id: IdSchema,
-  aliases: z.array(IdSchema).default([]),  // NEW v3
+  aliases: z.array(AliasSchema).default([]), // NEW v3
   name: NameSchema.optional(),           // often absent; derive from id
   name: NameSchema.optional(),           // often absent; derive from id
   description: DescriptionSchema,        // NEUTRAL INVARIANT — the only universal field
   description: DescriptionSchema,        // NEUTRAL INVARIANT — the only universal field
   tags: TagsSchema,
   tags: TagsSchema,
@@ -865,7 +871,7 @@ string alongside the normalized semver — so a no-op build does not churn ~289
 ```ts
 ```ts
 export const ContextSchema = z.object({
 export const ContextSchema = z.object({
   id: IdSchema,
   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
   /** 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
    *  carries NEITHER a name NOR a description, so every real context file failed
    *  validation). When absent, derived: name = title-cased path basename; description =
    *  validation). When absent, derived: name = title-cased path basename; description =
@@ -931,7 +937,7 @@ export const ToolParamSchema = z.object({
 
 
 export const ToolSchema = z.object({
 export const ToolSchema = z.object({
   id: IdSchema,
   id: IdSchema,
-  aliases: z.array(IdSchema).default([]),  // NEW v3
+  aliases: z.array(AliasSchema).default([]), // NEW v3
   name: NameSchema,
   name: NameSchema,
   description: DescriptionSchema,        // NEUTRAL — the model needs to know what it does
   description: DescriptionSchema,        // NEUTRAL — the model needs to know what it does
   tags: TagsSchema,
   tags: TagsSchema,
@@ -1045,7 +1051,7 @@ export const RegistryEntrySchema = z.object({
   /** Alternate ids resolvable in dependency refs (RESTORED v3 — 3 live entries use it;
   /** 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
    *  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). */
    *  if the symlink→alias collapse is chosen (index v2.2 symlink finding). */
-  aliases: z.array(IdSchema).default([]),
+  aliases: z.array(AliasSchema).default([]),
   name: NameSchema,
   name: NameSchema,
   type: ComponentTypeSchema,
   type: ComponentTypeSchema,
   /** Source path under /content/ (NOT .opencode/). NEUTRAL INVARIANT — catalog→source join. */
   /** 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. |
 | `tags[]` | string[] | Free-form labels. |
 | `capabilities` | see §0.4 | Ordered rule lists + scalar sugar. **INTENT, not any tool's syntax.** |
 | `capabilities` | see §0.4 | Ordered rule lists + scalar sugar. **INTENT, not any tool's syntax.** |
 | `inference.temperature` | number \| absent | Sampling temperature. |
 | `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. |
 | `inference.maxSteps` | number \| absent | Step cap. |
 | `context[]` | `{ path, priority?, description? }` | External context refs. See §0.6. |
 | `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. |
 | `examples[]` | `{ context, user, assistant }` | Few-shot delegation examples. |
 | `hooks[]` | `{ event, command, … }` | Lifecycle hooks. |
 | `hooks[]` | `{ event, command, … }` | Lifecycle hooks. |
 | body | markdown | System prompt authored once. |
 | 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
 ### 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()`
 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.
 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)
 ### 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
 - **Default-scope decision** — the decision of the rule whose scope is `*`, if present. This is
   what coarse targets (CC/Cursor/Windsurf) collapse to.
   what coarse targets (CC/Cursor/Windsurf) collapse to.
 - **Exceptions** — every rule whose scope ≠ `*`. These are what coarse targets lose.
 - **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:
 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.
 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
 > "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
 > system) … Rules are evaluated in order; the LAST matching rule wins. This matches OpenCode's
 > permission semantics exactly, ensuring IDE-native compatibility."
 > 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)
 ### 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` | `permission:` map | **ordered list → map, order preserved** (§1.3) |
 | `capabilities.delegate` | `permission.task` | `[{scope:X,decision:D}] → task: { X: "D" }` |
 | `capabilities.delegate` | `permission.task` | `[{scope:X,decision:D}] → task: { X: "D" }` |
 | `inference.temperature` | `temperature:` | verbatim |
 | `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 |
 | `inference.maxSteps` | sidecar (advisory) | no native field |
 | `context[]` | `@`-ref / `paths.json` + sidecar `context:<id>` dep | referenced, not inlined |
 | `context[]` | `@`-ref / `paths.json` + sidecar `context:<id>` dep | referenced, not inlined |
 | `dependencies[]` | sidecar `dependencies[]` | verbatim |
 | `dependencies[]` | sidecar `dependencies[]` | verbatim |
@@ -233,9 +222,10 @@ bash: [ {scope:"*",decision:deny},                      permission:
                                                              "bash …/router.sh status*": "allow"
                                                              "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.**
 **OpenCode is the only target that can express it.**
 
 
 **Ordering preservation — three caveats, in decreasing severity:**
 **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` | `tools:` / `disallowedTools:` | **ordered list → coarse allowlist** (§2.3) — lossy |
 | `capabilities.delegate` | `Task` in `tools:` | collapse per §2.3 |
 | `capabilities.delegate` | `Task` in `tools:` | collapse per §2.3 |
 | `inference.temperature` | **dropped** | warn |
 | `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 |
 | `inference.maxSteps` | dropped | warn |
 | `context[]` | copied → `context/` + SessionStart hook | **bundled**, not referenced (§2.4) |
 | `context[]` | copied → `context/` + SessionStart hook | **bundled**, not referenced (§2.4) |
 | `dependencies[]` | resolved & bundled | `subagent:` deps force-emit that agent file |
 | `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` | prose "# Tool Access" list | **default-scope decision only**; all scoping dropped (§3.3) |
 | `capabilities.delegate` | dropped | `task` unsupported (`ToolMapper` marks it so) |
 | `capabilities.delegate` | dropped | `task` unsupported (`ToolMapper` marks it so) |
 | `inference.temperature` | `temperature:` | verbatim; limited range |
 | `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 |
 | `context[]` | **inlined** into rules body | copied as prose; MVI comment travels inline verbatim |
 | `dependencies[]` | dropped | no dependency system |
 | `dependencies[]` | dropped | no dependency system |
 | `examples[]` | prose | no native support |
 | `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` | `tools` + `permissions` (binary) | **default-scope decision → boolean**; scoping dropped (§4.3) |
 | `capabilities.delegate` | `delegate` tool | partial (`task→delegate`) |
 | `capabilities.delegate` | `delegate` tool | partial (`task→delegate`) |
 | `inference.temperature` | `creativity` | `≤0.4→low`, `≤0.8→medium`, `>0.8→high` |
 | `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[]` | `contexts[]` path refs | referenced under `.windsurf/context/` |
 | `context[].priority` | `priority` | `critical/high→high`, `medium/low→low` (4→2) |
 | `context[].priority` | `priority` | `critical/high→high`, `medium/low→low` (4→2) |
 | `dependencies[]` | partial | subagent deps → separate JSONs; others dropped |
 | `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 |
 | 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) |
 | **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 |
 | **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 |
 | 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) |
 | 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 |
 | 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) |
 | 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 |
 | contextSubdirs (context) | full | full | none | full | cursor single-file |
 | skillsSystem (context) | full | full | none | partial | claude dir-per-skill; windsurf → context refs; cursor inline |
 | 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 |
 | temperatureControl (model) | full | none | partial | partial | claude drops; cursor limited; windsurf → creativity |
 | maxSteps (model) | full | none | none | none | oac sidecar advisory |
 | maxSteps (model) | full | none | none | none | oac sidecar advisory |
 | hooks — plugin/project-level (advanced) | full | full | none | none | claude: `hooks.json` + `session-start.sh` |
 | 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
 **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
 §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).
    and adding OpenCode as just another target (§1, §2).
 3. **Runtime must be Node-portable.** `bin/oac.js` currently `execFileSync('bun', …)` and the
 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
    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.
    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
 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
 `ConversionResult` with `configs[]` (path + content) and `warnings[]`. `CapabilityMatrix` predicts
 lossy transforms (temperature dropped for Claude, granular permissions flattened to a tool allowlist,
 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.
 All warnings surface to the user; `--verbose` lists each, otherwise a count.
 
 
 ### Stage 5 — Bundle context
 ### 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`).
   warns at 80KB / errors at 100KB for Cursor — that guard carries into `build`).
 
 
 ### Stage 6 — Write
 ### 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
 ### 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
 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
 ad-hoc collision scan and `update.sh`'s find-everything approach. `source` distinguishes
 `bundled` (neutral `/content` copied), `registry` (added component), `generated` (build output),
 `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
 ### 2.1 Determinism requirements
 - Stable sort of inputs (by id) before writing.
 - 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.
 - 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
 ### 2.2 Profiles
 Profiles (`essential`, `developer`, `business`, `full`, `advanced`) live in `registry.json`
 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
 `.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).
 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)
 ## 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
 - Default is **project-local** — the safe, no-surprise default. `--global`/`--dir` are explicit
   opt-ins (mirrors `install.sh`'s location menu but flag-driven).
   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
   escape the install root (`update.sh` already guarded `../` — preserve that: reject `..`/absolute
   escapes before any write).
   escapes before any write).
 - Global path today is `~/.config/opencode` on all platforms (from `get_global_install_path`).
 - 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 |
 | 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
 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`).
 run time (unlike `install.sh`, which curled every file from `raw.githubusercontent.com`).
 
 
 ### 4.5 Registry fetching, caching, offline
 ### 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 —
 - **`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**.
   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
 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
    the bundled neutral source so future `oac build` works — offering a diff if the user hand-edited
    `.opencode/` agents.
    `.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).
    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
 ### 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.
   surfaced by `readCliVersion()` and written into `.oac/manifest.json` `oacVersion` on init/update.
   `doctor` compares it to the npm-latest.
   `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**
 - **Manifest schema version** (`manifest.version: "1"`) and **registry schema version**
   (`registry.schema_version`) are independent of the product version and gate migrations.
   (`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
 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
 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.
 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) |
 | 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) |
 | 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 |
 | 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 |
 | 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 |
 | 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 |
 | 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)
 ## Stage 1 — Spec repair & decisions (docs only, no code)
 
 
 **Why:** `06-REVIEW`'s verdict is verified: the centerpiece schema rejects the corpus it
 **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
 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.
 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
 - 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`).
   marked *fixed* or *accepted* in a disposition table appended to `06`).
 - The precedence experiment's transcript committed to this directory.
 - 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).
   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.
    resolution; parse-time validation for integer-like scopes and duplicate scopes.
 
 
 **Tests / gate (all in CI from this stage on):**
 **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.
   and all 5 profiles. Zero failures, zero skips.
 - **Trap tests:** the line-232/line-301 prose-marker files parse with correct (path-derived)
 - **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
   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.
 written Stage-1 rules — not improvised under deadline.
 
 
 **Core features:**
 **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.
    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
 2. **Path tokenization** in bodies *and permission scopes* (`{{SKILL_ROOT}}` etc.) + a lint
    forbidding raw `.opencode/` paths in `/content/` (~129 files affected).
    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
 ## 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
 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
 (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
 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
 > **Status:** Decided — written BEFORE Stage 4 executes (per `06-REVIEW.md` recommendation #3
 > and `07-EXECUTION-PLAN.md` Stage 4 feature 1).
 > 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`.
 > **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
 > 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
 > (branch `refactor/canonical`). Where disk disagrees with earlier docs, disk wins and the
 > discrepancy is recorded in §9.
 > 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/`. |
 | `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). |
 | `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
 ### 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
 ### 3.4 Identity
 
 
 Canonical id = CC kebab-case filename (`01` Q1 disposition, `06-REVIEW.md:778`: *filename ==
 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 | ✅ |
 | 00–06 | Spec set + adversarial review | ✅ |
 | 07 | Execution plan (stages, gates, kill criteria) | ✅ |
 | 07 | Execution plan (stages, gates, kill criteria) | ✅ |
 | 08 | Repo structure, build flow, packaging (`@controlstack/oac`) | ✅ |
 | 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 | ✅ |
 | 12 | This dispatch guide | ✅ |
 
 
 Where a subtask brief and a spec doc conflict, precedence is:
 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.
 - `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
 - `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.
   `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`).
 - 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.
   `.claude-plugin/marketplace.json` is live production config — touch only when a brief says so.