@ericrisco/rsc 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,6 +150,11 @@ rsc consult "I want to launch a SaaS" # recommend only, no install
150
150
  rsc registry refresh # write .rsc/skill-registry.{json,md}
151
151
  rsc list # what rsc has installed
152
152
  rsc doctor # health check (state, hook, counts)
153
+ rsc sync --target claude,codex # refresh managed skills/hooks from the current package version
154
+ rsc backups # list project-local snapshots
155
+ rsc restore latest --dry-run # preview restoring the newest snapshot
156
+ rsc restore <snapshot-id> # restore a project-local snapshot
157
+ rsc upgrade --dry-run # show npm upgrade + sync commands
153
158
  rsc uninstall postgresdb --dry-run # preview a removal
154
159
  ```
155
160
 
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.21",
2
+ "version": "0.1.23",
3
3
  "counts": {
4
4
  "skills": 231
5
5
  },
@@ -3666,7 +3666,7 @@
3666
3666
  },
3667
3667
  {
3668
3668
  "id": "sdd",
3669
- "description": "Use when you want a disciplined, spec-driven path from a feature idea to shipped, verified software — the rsc-sdd dispatcher / front door. It states the SDD method, reads the accompaniment dial from 02-DOCS, and routes to the right phase skill: constitution -> specify -> clarify -> plan -> tasks -> analyze -> implement -> verify -> review -> ship, with debug / worktrees / parallel callable on demand. Use it to START a feature, when unsure which SDD phase you are in, or to govern the whole flow. Triggers: 'spec-driven development', 'sdd', 'build this feature properly', 'start a new feature', 'I have an idea, take it to production', 'which phase am I in', 'run the sdd flow', 'desarrollo dirigido por especificación', 'monta esta feature bien', 'de la idea a producción'. NOT itself a single phase (it dispatches), NOT the workspace harness (harness), NOT a stack build skill.",
3669
+ "description": "Use when you want a disciplined, spec-driven path from a feature idea to shipped, verified software — the rsc-sdd dispatcher / front door. It states the SDD method, reads the accompaniment dial from 02-DOCS, and routes to the right phase skill: constitution -> specify -> clarify -> plan -> tasks -> analyze -> implement -> verify -> review -> ship, with debug / worktrees / parallel callable on demand. Use it to START a feature, when unsure which SDD phase you are in, or to govern the whole flow. Triggers: 'spec-driven development', 'sdd', 'build this feature properly', 'start a new feature', 'I have an idea, take it to production', 'which phase am I in', 'run the sdd flow', 'desarrollo dirigido por especificación', 'monta esta feature bien', 'de la idea a producción', 'per-phase model routing', 'use a cheaper model for implementation', 'qué modelo por fase'. NOT itself a single phase (it dispatches), NOT the workspace harness (harness), NOT a stack build skill.",
3670
3670
  "tags": [
3671
3671
  "sdd",
3672
3672
  "spec",
@@ -3685,7 +3685,7 @@
3685
3685
  },
3686
3686
  {
3687
3687
  "id": "sdd-init",
3688
- "description": "Use when calibrating an existing repo before running the rsc SDD flow: detecting stack, package manager, test runners, lint/type/build commands, monorepo signals, artifact store, execution mode, review budget, strict TDD capability, and project skill registry. Triggers: 'calibrate this repo for SDD', 'run sdd init', 'detect my test runner before implementing', 'set up SDD config', 'prepare this repo for spec-driven development'. NOT first-contact user/workspace bootstrap (init), NOT 01-TOOLS/02-DOCS scaffolding (harness), NOT writing a feature spec (specify).",
3688
+ "description": "Use when calibrating an existing repo before running the rsc SDD flow: detecting stack, package manager, test runners, lint/type/build commands, monorepo signals, artifact store, execution mode, review budget, strict TDD capability, and project skill registry. Triggers: 'calibrate this repo for SDD', 'run sdd init', 'detect my test runner before implementing', 'set up SDD config', 'prepare this repo for spec-driven development', 'configure per-phase model routing', 'assign models per SDD phase', 'set up cheaper model for implementation'. NOT first-contact user/workspace bootstrap (init), NOT 01-TOOLS/02-DOCS scaffolding (harness), NOT writing a feature spec (specify).",
3689
3689
  "tags": [
3690
3690
  "sdd",
3691
3691
  "init",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/doctor.js CHANGED
@@ -1,18 +1,27 @@
1
1
  import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
2
3
  import { targetPaths } from '../targets/index.js';
3
4
  import { readState } from './lib/state.js';
4
5
  import { loadManifest } from './lib/manifest.js';
6
+ import { listBackups } from './lib/backups.js';
5
7
 
6
8
  export function doctor({ target, home, cwd }) {
9
+ const root = cwd || process.cwd();
7
10
  const paths = targetPaths(target, home, cwd);
8
11
  const state = readState(paths.stateFile);
9
12
  const manifest = loadManifest();
13
+ const backups = listBackups({ cwd: root });
10
14
  const report = {
11
15
  target,
12
16
  installed: Object.keys(state.skills),
13
17
  missing: [],
14
18
  hookWired: existsSync(paths.hookTarget),
15
19
  manifestSkills: manifest.counts.skills,
20
+ backups: {
21
+ exists: existsSync(join(root, '.rsc', 'backups')),
22
+ count: backups.length,
23
+ latest: backups[0]?.id || null,
24
+ },
16
25
  };
17
26
  for (const [id, e] of Object.entries(state.skills)) {
18
27
  for (const f of e.files) if (!existsSync(f)) report.missing.push(`${id}:${f}`);
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { planInstall } from './install-plan.js';
6
6
  import { targetPaths, writeSkill, wireHook, unwireHook, baseDir, TARGET_IDS } from '../targets/index.js';
7
7
  import { readState, writeState } from './lib/state.js';
8
+ import { createBackup } from './lib/backups.js';
8
9
 
9
10
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
10
11
  const CLI_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
@@ -32,10 +33,37 @@ function ensureBase(id, cwd, refresh) {
32
33
  return dest;
33
34
  }
34
35
 
35
- export async function applyInstall({ skillIds, target, home, cwd = process.cwd() }) {
36
+ function generatedHookFiles({ target, cwd }) {
37
+ if (target !== 'claude') return [];
38
+ return [
39
+ join(cwd, '.rsc', 'session-start.mjs'),
40
+ join(cwd, '.rsc', 'worklog-checkpoint.mjs'),
41
+ join(cwd, '.rsc', 'ship-guard.mjs'),
42
+ join(cwd, '.rsc', 'danger-guard.mjs'),
43
+ ];
44
+ }
45
+
46
+ function managedPathsForInstall({ skillIds, target, home, cwd }) {
47
+ const paths = targetPaths(target, home, cwd);
48
+ const plan = planInstall({ skillIds, target, home, cwd });
49
+ const out = [paths.stateFile, versionFile(cwd)];
50
+ for (const step of plan) {
51
+ if (step.kind === 'skill') {
52
+ out.push(step.to, baseDir(step.id, cwd));
53
+ } else if (step.kind === 'hook') {
54
+ out.push(step.to, ...generatedHookFiles({ target, cwd }));
55
+ }
56
+ }
57
+ return [...new Set(out)];
58
+ }
59
+
60
+ export async function applyInstall({ skillIds, target, home, cwd = process.cwd(), operation = 'install', dryRun = false }) {
36
61
  const paths = targetPaths(target, home, cwd);
37
62
  const plan = planInstall({ skillIds, target, home, cwd });
63
+ const managedPaths = managedPathsForInstall({ skillIds, target, home, cwd });
64
+ if (dryRun) return { dryRun: true, skills: skillIds, paths: managedPaths };
38
65
  const state = readState(paths.stateFile);
66
+ const backup = createBackup({ cwd, operation, target, paths: managedPaths, cliVersion: CLI_VERSION });
39
67
  // Refresh bases when installing a different version than they were materialized at
40
68
  // (or a pre-versioning install where the marker is absent). Same version → no-op.
41
69
  const refresh = readBaseVersion(cwd) !== CLI_VERSION;
@@ -52,7 +80,7 @@ export async function applyInstall({ skillIds, target, home, cwd = process.cwd()
52
80
  writeState(paths.stateFile, state);
53
81
  mkdirSync(dirname(versionFile(cwd)), { recursive: true });
54
82
  writeFileSync(versionFile(cwd), CLI_VERSION + '\n');
55
- return state;
83
+ return { ...state, backup };
56
84
  }
57
85
 
58
86
  export function listInstalled({ target, home, cwd = process.cwd() }) {
@@ -64,23 +92,52 @@ export async function uninstall({ skillIds, target, home, cwd = process.cwd(), d
64
92
  const paths = targetPaths(target, home, cwd);
65
93
  const state = readState(paths.stateFile);
66
94
  const removed = [];
95
+ const managedPaths = [paths.stateFile];
67
96
  for (const id of skillIds) {
68
97
  const entry = state.skills[id];
69
98
  if (!entry) continue;
70
99
  for (const f of entry.files) {
100
+ managedPaths.push(f);
71
101
  removed.push(f);
72
- if (!dryRun && existsSync(f)) rmSync(f, { recursive: true, force: true });
73
102
  }
74
- if (!dryRun) delete state.skills[id];
75
103
  }
76
- if (!dryRun) writeState(paths.stateFile, state);
104
+ if (!removed.length) return removed;
105
+ if (dryRun) return removed;
106
+ createBackup({ cwd, operation: 'uninstall', target, paths: managedPaths, cliVersion: CLI_VERSION });
107
+ for (const id of skillIds) {
108
+ const entry = state.skills[id];
109
+ if (!entry) continue;
110
+ for (const f of entry.files) {
111
+ if (existsSync(f)) rmSync(f, { recursive: true, force: true });
112
+ }
113
+ delete state.skills[id];
114
+ }
115
+ writeState(paths.stateFile, state);
77
116
  return removed;
78
117
  }
79
118
 
119
+ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun = false }) {
120
+ const paths = targetPaths(target, home, cwd);
121
+ const state = readState(paths.stateFile);
122
+ const ids = Object.keys(state.skills || {});
123
+ if (!ids.length) return dryRun ? { dryRun: true, synced: [], paths: [] } : { synced: [], backup: null };
124
+ if (dryRun) {
125
+ return {
126
+ dryRun: true,
127
+ synced: ids,
128
+ paths: managedPathsForInstall({ skillIds: ids, target, home, cwd }),
129
+ };
130
+ }
131
+ const nextState = await applyInstall({ skillIds: ids, target, home, cwd, operation: 'sync' });
132
+ return { synced: ids, backup: nextState.backup };
133
+ }
134
+
80
135
  // Remove EVERYTHING rsc put in this project: installed skills across all targets,
81
136
  // the wired hooks (settings.json entries / AGENTS-blocks / cursor rules), and the
82
137
  // shared `.rsc/` (base + hook scripts + version marker). `02-DOCS/` is the user's
83
138
  // own knowledge — kept unless `withDocs` is set. Returns the paths touched.
139
+ // Note: backups live under `.rsc/backups/`, which this removes — so purge does not
140
+ // snapshot (a pre-purge backup would delete itself). It is the deliberate escape hatch.
84
141
  export async function purge({ home, cwd = process.cwd(), withDocs = false, dryRun = false } = {}) {
85
142
  const removed = [];
86
143
  const drop = (p, recursive = false) => {
@@ -0,0 +1,154 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ readlinkSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import { dirname, isAbsolute, join, relative, sep } from 'node:path';
14
+
15
+ const SCHEMA_VERSION = 1;
16
+
17
+ export function backupsDir(cwd = process.cwd()) {
18
+ return join(cwd, '.rsc', 'backups');
19
+ }
20
+
21
+ export function createBackup({ cwd = process.cwd(), operation, target, paths, cliVersion, now = new Date() }) {
22
+ const uniquePaths = [...new Set((paths || []).filter(Boolean))];
23
+ const id = uniqueSnapshotId({ cwd, now, operation, target });
24
+ const root = join(backupsDir(cwd), id);
25
+ const filesRoot = join(root, 'files');
26
+ mkdirSync(filesRoot, { recursive: true });
27
+
28
+ const entries = uniquePaths.map((absPath) => snapshotEntry({ cwd, root, absPath }));
29
+ const manifest = {
30
+ schemaVersion: SCHEMA_VERSION,
31
+ id,
32
+ createdAt: now.toISOString(),
33
+ operation,
34
+ target,
35
+ cwd,
36
+ cliVersion,
37
+ entries,
38
+ };
39
+ writeFileSync(join(root, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
40
+ return manifest;
41
+ }
42
+
43
+ export function listBackups({ cwd = process.cwd() } = {}) {
44
+ const dir = backupsDir(cwd);
45
+ if (!existsSync(dir)) return [];
46
+ return readdirSync(dir)
47
+ .map((id) => readManifest({ cwd, id }))
48
+ .filter(Boolean)
49
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
50
+ }
51
+
52
+ export function restoreBackup({ cwd = process.cwd(), id, dryRun = false }) {
53
+ const snapshot = resolveSnapshot({ cwd, id });
54
+ const changed = [];
55
+ for (const entry of snapshot.entries) {
56
+ const absPath = safeJoin(cwd, entry.path);
57
+ changed.push(absPath);
58
+ if (dryRun) continue;
59
+ restoreEntry({ cwd, snapshot, entry, absPath });
60
+ }
61
+ return { snapshot, changed };
62
+ }
63
+
64
+ function snapshotEntry({ cwd, root, absPath }) {
65
+ const rel = safeRelative(cwd, absPath);
66
+ if (!existsSync(absPath)) return { path: rel, existed: false, kind: 'missing' };
67
+
68
+ const stat = lstatSync(absPath);
69
+ if (stat.isSymbolicLink()) {
70
+ return { path: rel, existed: true, kind: 'symlink', linkTarget: readlinkSync(absPath) };
71
+ }
72
+
73
+ const contentPath = join('files', rel);
74
+ const contentAbs = join(root, contentPath);
75
+ mkdirSync(dirname(contentAbs), { recursive: true });
76
+ if (stat.isDirectory()) {
77
+ cpSync(absPath, contentAbs, { recursive: true });
78
+ return { path: rel, existed: true, kind: 'dir', contentPath };
79
+ }
80
+
81
+ cpSync(absPath, contentAbs);
82
+ return { path: rel, existed: true, kind: 'file', contentPath };
83
+ }
84
+
85
+ function restoreEntry({ cwd, snapshot, entry, absPath }) {
86
+ if (!entry.existed) {
87
+ rmSync(absPath, { recursive: true, force: true });
88
+ return;
89
+ }
90
+
91
+ rmSync(absPath, { recursive: true, force: true });
92
+ mkdirSync(dirname(absPath), { recursive: true });
93
+ if (entry.kind === 'symlink') {
94
+ symlinkSync(entry.linkTarget, absPath, process.platform === 'win32' ? 'junction' : undefined);
95
+ return;
96
+ }
97
+
98
+ const contentAbs = join(backupsDir(cwd), snapshot.id, entry.contentPath);
99
+ cpSync(contentAbs, absPath, { recursive: entry.kind === 'dir' });
100
+ }
101
+
102
+ function resolveSnapshot({ cwd, id }) {
103
+ if (!id) throw new Error('restore requires a snapshot id or latest');
104
+ if (id === 'latest') {
105
+ const latest = listBackups({ cwd })[0];
106
+ if (!latest) throw new Error('no backups found');
107
+ return latest;
108
+ }
109
+
110
+ const manifest = readManifest({ cwd, id });
111
+ if (!manifest) throw new Error(`backup not found: ${id}`);
112
+ return manifest;
113
+ }
114
+
115
+ function readManifest({ cwd, id }) {
116
+ const path = join(backupsDir(cwd), id, 'manifest.json');
117
+ if (!existsSync(path)) return undefined;
118
+ return JSON.parse(readFileSync(path, 'utf8'));
119
+ }
120
+
121
+ function uniqueSnapshotId({ cwd, now, operation, target }) {
122
+ const base = snapshotId({ now, operation, target });
123
+ let id = base;
124
+ let counter = 2;
125
+ while (existsSync(join(backupsDir(cwd), id))) {
126
+ id = `${base}-${counter}`;
127
+ counter += 1;
128
+ }
129
+ return id;
130
+ }
131
+
132
+ function snapshotId({ now, operation, target }) {
133
+ const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\..*/, '').replace('T', '-');
134
+ return `${stamp}-${safeId(operation)}-${safeId(target || 'all')}`;
135
+ }
136
+
137
+ function safeId(value) {
138
+ return String(value).replace(/[^a-z0-9._-]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase();
139
+ }
140
+
141
+ function safeRelative(cwd, absPath) {
142
+ const rel = relative(cwd, absPath);
143
+ if (!rel || rel.startsWith('..') || rel.split(sep).includes('..')) {
144
+ throw new Error(`path is outside project root: ${absPath}`);
145
+ }
146
+ return rel.split(sep).join('/');
147
+ }
148
+
149
+ function safeJoin(cwd, relPath) {
150
+ if (!relPath || isAbsolute(relPath) || relPath.split('/').includes('..')) {
151
+ throw new Error(`path is outside project root: ${relPath}`);
152
+ }
153
+ return join(cwd, ...relPath.split('/'));
154
+ }
@@ -0,0 +1,18 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ export function upgradePlan({ targets = [] } = {}) {
4
+ const targetArg = targets.length ? targets.join(',') : '<target>';
5
+ return {
6
+ installCommand: 'npm install -g @ericrisco/rsc@latest',
7
+ syncCommand: `rsc sync --target ${targetArg}`,
8
+ };
9
+ }
10
+
11
+ export function runUpgrade({ targets = [], dryRun = false, global = false } = {}) {
12
+ const plan = upgradePlan({ targets });
13
+ if (dryRun || !global) return { ran: false, plan };
14
+
15
+ const result = spawnSync('npm', ['install', '-g', '@ericrisco/rsc@latest'], { stdio: 'inherit' });
16
+ if (result.status !== 0) throw new Error('npm global upgrade failed');
17
+ return { ran: true, plan };
18
+ }
package/scripts/rsc.js CHANGED
@@ -4,12 +4,14 @@ import { detectTarget, TARGETS } from '../targets/index.js';
4
4
  import { detectRepo } from './detect-repo.js';
5
5
  import { rank } from './consult.js';
6
6
  import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
7
- import { applyInstall, listInstalled, uninstall, purge } from './install-apply.js';
7
+ import { applyInstall, listInstalled, uninstall, syncInstalled, purge } from './install-apply.js';
8
8
  import { doctor } from './doctor.js';
9
9
  import { say, select, pickFrom, banner, confirm } from './lib/ui.js';
10
10
  import { refreshRegistry, registryStatus } from './lib/registry.js';
11
11
  import { audit, writeAuditReport } from './audit.js';
12
12
  import { DOMAINS } from './lib/domains.js';
13
+ import { listBackups, restoreBackup } from './lib/backups.js';
14
+ import { runUpgrade } from './lib/upgrade.js';
13
15
 
14
16
  const argv = process.argv.slice(2);
15
17
  const cmd = argv[0];
@@ -200,6 +202,43 @@ async function main() {
200
202
  return void say(listInstalled({ target }).join('\n') || '(nothing installed)');
201
203
  case 'doctor':
202
204
  return void say(JSON.stringify(doctor({ target }), null, 2));
205
+ case 'sync': {
206
+ const dry = argv.includes('--dry-run');
207
+ for (const t of targets) {
208
+ const result = await syncInstalled({ target: t, dryRun: dry });
209
+ const verb = dry ? 'Would sync' : 'Synced';
210
+ say(`${verb} ${t}: ${result.synced.length ? result.synced.join(', ') : '(nothing to sync)'}`);
211
+ if (dry && result.paths?.length) {
212
+ for (const p of result.paths) say(` ${p}`);
213
+ }
214
+ }
215
+ return;
216
+ }
217
+ case 'backups': {
218
+ const backups = listBackups();
219
+ if (!backups.length) return void say('(no backups)');
220
+ for (const b of backups) {
221
+ say(`${b.id}\t${b.operation}\t${b.target}\t${b.entries.length} files\t${b.createdAt}`);
222
+ }
223
+ return;
224
+ }
225
+ case 'restore': {
226
+ const dry = argv.includes('--dry-run');
227
+ const id = argv.slice(1).find((a) => !a.startsWith('--'));
228
+ const result = restoreBackup({ id, dryRun: dry });
229
+ say(`${dry ? 'Would restore' : 'Restored'} ${result.snapshot.id}`);
230
+ for (const p of result.changed) say(` ${p}`);
231
+ return;
232
+ }
233
+ case 'upgrade': {
234
+ const dry = argv.includes('--dry-run');
235
+ const global = argv.includes('--global');
236
+ const result = runUpgrade({ targets, dryRun: dry, global });
237
+ if (result.ran) say('Upgraded global @ericrisco/rsc. Restart your shell if needed.');
238
+ else say(`${dry ? 'Would run' : 'Upgrade guide'}: ${result.plan.installCommand}`);
239
+ say(`After upgrade: ${result.plan.syncCommand}`);
240
+ return;
241
+ }
203
242
  case 'registry': {
204
243
  const sub = argv[1];
205
244
  if (sub === 'refresh') {
@@ -226,7 +265,7 @@ async function main() {
226
265
  return void (await runPurge(argv.includes('--dry-run'), argv.includes('--with-docs')));
227
266
  default:
228
267
  say(`rsc: unknown command '${cmd}'.`);
229
- say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | audit | registry refresh | doctor | uninstall <id> | purge');
268
+ say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | audit | registry refresh | doctor | sync | backups | restore <id|latest> | upgrade | uninstall <id> | purge');
230
269
  }
231
270
  }
232
271
 
@@ -36,6 +36,10 @@ Do NOT use when (route elsewhere):
36
36
  - A test is failing or behavior is wrong → `debug`.
37
37
  - You intend to fix what you find right now → that is `implement`/`clarify`/`plan`/`tasks` work, not analyze. Analyze only reports.
38
38
 
39
+ ## Model tier — `heavy` (opt-in routing)
40
+
41
+ This phase's default model tier is **`heavy`** — it is the adversarial consistency gate across constitution ↔ spec ↔ plan ↔ tasks. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
42
+
39
43
  ## Read the accompaniment dial first
40
44
 
41
45
  Before reporting, read `02-DOCS/wiki/harness/user-profile.md` for the technical + accompaniment level (the harness dial, L0..L3). It shapes how the report reads — not what gets checked. The six analyses always run in full; verbosity flexes:
@@ -88,6 +88,10 @@ How you ask determines whether you get a usable answer.
88
88
  - **One batch, ranked, then stop.** Don't drip questions one at a time over many turns unless the dial is L3. Don't dump thirty at once. Ask the few that matter, together.
89
89
  - **Quote the spec.** Anchor each question to the exact line or section it came from, so the user sees *why* it's open.
90
90
 
91
+ ## Model tier — `balanced` (opt-in routing)
92
+
93
+ This phase's default model tier is **`balanced`** — it ranks and asks the few high-leverage questions, not architecture. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
94
+
91
95
  ## The accompaniment dial
92
96
 
93
97
  Read the level from `02-DOCS/wiki/harness/user-profile.md` and adapt **how many questions and how you frame them** — clarify is question-heavy, so the dial matters here more than almost anywhere:
@@ -15,6 +15,10 @@ A constitution is small, durable, and enforceable. It is **not** a wiki of every
15
15
 
16
16
  This skill produces `02-DOCS/wiki/sdd/constitution.md` and one Knowledge-map row in the root `CLAUDE.md`. It reconciles with — never duplicates — the stack conventions the harness keeps under `02-DOCS/wiki/stack/*`.
17
17
 
18
+ ## Model tier — `heavy` (opt-in routing)
19
+
20
+ This phase's default model tier is **`heavy`** — it sets the project's non-negotiables, the highest-leverage decisions in the repo. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
21
+
18
22
  ## Honor the accompaniment dial first
19
23
 
20
24
  Before asking anything, read `02-DOCS/wiki/harness/user-profile.md` and match its `technical_level` and `accompaniment_level`. The constitution interview adapts:
@@ -46,6 +46,10 @@ Do NOT use when:
46
46
  - The "bug" is actually an unclear requirement or contradictory spec — that's `clarify`/`analyze`,
47
47
  not a code defect.
48
48
 
49
+ ## Model tier — `heavy` (opt-in routing)
50
+
51
+ This phase's default model tier is **`heavy`** — root-cause diagnosis is deep reasoning. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
52
+
49
53
  ## Read the room first (accompaniment dial)
50
54
 
51
55
  Before diagnosing, read `02-DOCS/wiki/harness/user-profile.md` for the technical + accompaniment
@@ -158,6 +158,10 @@ test output. You merge the branches, then run the *combined* test suite before t
158
158
  green-in-isolation is not green-together. Hand the orchestration to `parallel`; keep the TDD
159
159
  discipline inside each branch.
160
160
 
161
+ ## Model tier — `balanced` (opt-in routing)
162
+
163
+ This phase's default model tier is **`balanced`** — it is the bulk of TDD execution: cost-sensitive, with quality balanced handles well. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model (this is where routing pays off most — fan-out runs on `balanced` while a hard sub-problem can be escalated to `heavy`). Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
164
+
161
165
  ## The accompaniment dial — how loud at each checkpoint
162
166
 
163
167
  Read the level from `02-DOCS/wiki/harness/user-profile.md` and match it. Same work, different volume.
@@ -65,6 +65,7 @@ SUBAGENT BRIEF (one per unit)
65
65
  - Compact rules — 4-5 actionable rules digested from those skills for THIS unit
66
66
  - Skill fallback — what to do if a referenced skill is unavailable
67
67
  - Interface — any contract it must conform to, FROZEN before dispatch (see the rule below)
68
+ - Model tier — the tier this unit runs on, by the KIND of work it does (only when routing is enabled — see below)
68
69
  - Report-back — what to return: the diff, the test output, decisions worth logging, skill_resolution
69
70
  ```
70
71
 
@@ -72,6 +73,8 @@ SUBAGENT BRIEF (one per unit)
72
73
 
73
74
  Each subagent still owns its own discipline inside its scope — TDD via `implement`, the stack skill's test mechanics, decision logging. This skill does not relax any of that; it just runs several of them at once.
74
75
 
76
+ **Per-unit model tier (when routing is enabled).** `parallel` has *no fixed tier* — this is the most concrete place per-phase model routing pays off. When `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`, give each unit the tier of the *kind of work it does*, not one tier for the whole fan-out: an implement-type unit → `balanced`, a scan/research/boilerplate unit → `light`, a unit doing genuine design or root-cause reasoning → `heavy`. Resolve the tier to a concrete model via `models.tiers` and **dispatch that subagent on that model** (e.g. Claude Code's `model` field on the Task/subagent) — real routing, independent of the session model. If routing is off or no profile exists, dispatch on the session model and say nothing. Full protocol: `../sdd/references/model-routing.md`.
77
+
75
78
  ### Skill resolution feedback
76
79
 
77
80
  Every subagent result must report:
@@ -137,6 +140,7 @@ Be honest about this — most task lists are *mostly* serial with a few disjoint
137
140
  | "The brief says 'see the other agent's output' — close enough." | That's a dependency, not independence. Serialize, or freeze the output first. |
138
141
  | "Combined suite is red, probably the slower unit — I'll tweak it." | Don't guess at the seam. Reproduce and isolate with debug. |
139
142
  | "I just want an isolated branch, so I'll use parallel." | That's isolation, not concurrency. Use worktrees. |
143
+ | "Routing's on, so I'll run the whole fan-out on one tier." | `parallel` has no fixed tier — give each unit the tier of its own work (scan→light, build→balanced, design→heavy). |
140
144
 
141
145
  ## Red flags — stop and re-plan the partition
142
146
 
@@ -172,6 +176,7 @@ End with:
172
176
  "artifact": "02-DOCS/wiki/sdd/progress/<slug>.md",
173
177
  "next_recommended": "implement",
174
178
  "risk": "low|medium|high",
179
+ "model": { "per_unit": [{ "unit": "users-repo", "tier": "balanced", "resolved": "model-id" }], "routing": "on|off" },
175
180
  "skill_resolution": {
176
181
  "used": ["parallel"],
177
182
  "missing": [],
@@ -150,6 +150,10 @@ mitigation or the spike that would retire it. Separately, log any decision still
150
150
  the riskiest plan. Significant design decisions taken during planning also get appended to
151
151
  `02-DOCS/wiki/sdd/decisions.md` (the SDD decisions log), so later phases can trace the *why*.
152
152
 
153
+ ## Model tier — `heavy` (opt-in routing)
154
+
155
+ This phase's default model tier is **`heavy`** — architecture, interfaces, data flow and risks are the heaviest cognitive load in the chain. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
156
+
153
157
  ## Adapting to the dial
154
158
 
155
159
  Read `02-DOCS/wiki/harness/user-profile.md` and match the artifact and your narration to it:
@@ -184,6 +184,10 @@ When you've processed the review, summarize for the reviewer (and the decisions
184
184
 
185
185
  ---
186
186
 
187
+ ## Model tier — `heavy` (opt-in routing)
188
+
189
+ This phase's default model tier is **`heavy`** — adversarial diff reading is where the strongest model pays off most. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
190
+
187
191
  ## Accompaniment dial (L0..L3)
188
192
 
189
193
  Read the level from `02-DOCS/wiki/harness/user-profile.md`. **It changes the narration, never the rigor** — every level runs the same passes and the same evidence bar.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sdd
3
- description: "Use when you want a disciplined, spec-driven path from a feature idea to shipped, verified software — the rsc-sdd dispatcher / front door. It states the SDD method, reads the accompaniment dial from 02-DOCS, and routes to the right phase skill: constitution -> specify -> clarify -> plan -> tasks -> analyze -> implement -> verify -> review -> ship, with debug / worktrees / parallel callable on demand. Use it to START a feature, when unsure which SDD phase you are in, or to govern the whole flow. Triggers: 'spec-driven development', 'sdd', 'build this feature properly', 'start a new feature', 'I have an idea, take it to production', 'which phase am I in', 'run the sdd flow', 'desarrollo dirigido por especificación', 'monta esta feature bien', 'de la idea a producción'. NOT itself a single phase (it dispatches), NOT the workspace harness (harness), NOT a stack build skill."
3
+ description: "Use when you want a disciplined, spec-driven path from a feature idea to shipped, verified software — the rsc-sdd dispatcher / front door. It states the SDD method, reads the accompaniment dial from 02-DOCS, and routes to the right phase skill: constitution -> specify -> clarify -> plan -> tasks -> analyze -> implement -> verify -> review -> ship, with debug / worktrees / parallel callable on demand. Use it to START a feature, when unsure which SDD phase you are in, or to govern the whole flow. Triggers: 'spec-driven development', 'sdd', 'build this feature properly', 'start a new feature', 'I have an idea, take it to production', 'which phase am I in', 'run the sdd flow', 'desarrollo dirigido por especificación', 'monta esta feature bien', 'de la idea a producción', 'per-phase model routing', 'use a cheaper model for implementation', 'qué modelo por fase'. NOT itself a single phase (it dispatches), NOT the workspace harness (harness), NOT a stack build skill."
4
4
  tags: [sdd, spec, workflow, plan]
5
5
  recommends: [sdd-init, constitution, specify]
6
6
  profiles: [core, full]
@@ -46,23 +46,27 @@ constitution ─(once per project)─┐
46
46
  debug · worktrees · parallel
47
47
  ```
48
48
 
49
- | Phase | Owns | Writes | Sibling skill |
50
- | --- | --- | --- | --- |
51
- | **sdd-init** | Technical runtime calibration: stack, tests, commands, registry, budgets | `02-DOCS/wiki/sdd/config.yaml`, `.rsc/skill-registry.*` | `../sdd-init/SKILL.md` |
52
- | **proposal** | Optional pre-execution briefing for ambiguous/architectural/risky work | `02-DOCS/wiki/sdd/proposals/<slug>.md` | handled by `../specify/SKILL.md` when needed |
53
- | **constitution** | Project non-negotiables: stack canon, quality bars, conventions | `02-DOCS/wiki/sdd/constitution.md` | `../constitution/SKILL.md` |
54
- | **specify** | Turn a fuzzy intent into a spec — what & why, no how | `02-DOCS/wiki/sdd/specs/<slug>.md` | `../specify/SKILL.md` |
55
- | **clarify** | Surface ambiguities / edge cases, ask, bake answers back in | updates the spec | `../clarify/SKILL.md` |
56
- | **plan** | Technical plan: architecture, interfaces, data flow, tests, risks | `02-DOCS/wiki/sdd/plans/<slug>.md` | `../plan/SKILL.md` |
57
- | **tasks** | Break the plan into ordered, independently-verifiable tasks | task list in the plan artifact | `../tasks/SKILL.md` |
58
- | **analyze** | Consistency gate: constitution ↔ spec ↔ plan ↔ tasks (report only) | a gap report | `../analyze/SKILL.md` |
59
- | **implement** | Execute tasks with checkpoints; TDD discipline embedded | logs to `02-DOCS/wiki/sdd/decisions.md` | `../implement/SKILL.md` |
60
- | **verify** | Post-build gate: run the stack's checks + done-checks + acceptance | evidence | `../verify/SKILL.md` |
61
- | **review** | Adversarial code review — give and receive with rigor | review notes | `../review/SKILL.md` |
62
- | **ship** | Close the branch: PR / merge / cleanup. **Git authorship = Eric** | the merge/PR and archive bundle | `../ship/SKILL.md` |
63
- | **debug** | Root-cause diagnosis: reproduce → isolate → fix → verify | a diagnosis | `../debug/SKILL.md` |
64
- | **worktrees** | Isolate feature work in a branch/worktree before executing a plan | an isolated workspace | `../worktrees/SKILL.md` |
65
- | **parallel** | Fan out independent tasks across subagents, gather results | merged results | `../parallel/SKILL.md` |
49
+ | Phase | Owns | Writes | Tier | Sibling skill |
50
+ | --- | --- | --- | --- | --- |
51
+ | **sdd-init** | Technical runtime calibration: stack, tests, commands, registry, budgets | `02-DOCS/wiki/sdd/config.yaml`, `.rsc/skill-registry.*` | light | `../sdd-init/SKILL.md` |
52
+ | **proposal** | Optional pre-execution briefing for ambiguous/architectural/risky work | `02-DOCS/wiki/sdd/proposals/<slug>.md` | — | handled by `../specify/SKILL.md` when needed |
53
+ | **constitution** | Project non-negotiables: stack canon, quality bars, conventions | `02-DOCS/wiki/sdd/constitution.md` | heavy | `../constitution/SKILL.md` |
54
+ | **specify** | Turn a fuzzy intent into a spec — what & why, no how | `02-DOCS/wiki/sdd/specs/<slug>.md` | balanced | `../specify/SKILL.md` |
55
+ | **clarify** | Surface ambiguities / edge cases, ask, bake answers back in | updates the spec | balanced | `../clarify/SKILL.md` |
56
+ | **plan** | Technical plan: architecture, interfaces, data flow, tests, risks | `02-DOCS/wiki/sdd/plans/<slug>.md` | heavy | `../plan/SKILL.md` |
57
+ | **tasks** | Break the plan into ordered, independently-verifiable tasks | task list in the plan artifact | balanced | `../tasks/SKILL.md` |
58
+ | **analyze** | Consistency gate: constitution ↔ spec ↔ plan ↔ tasks (report only) | a gap report | heavy | `../analyze/SKILL.md` |
59
+ | **implement** | Execute tasks with checkpoints; TDD discipline embedded | logs to `02-DOCS/wiki/sdd/decisions.md` | balanced | `../implement/SKILL.md` |
60
+ | **verify** | Post-build gate: run the stack's checks + done-checks + acceptance | evidence | balanced | `../verify/SKILL.md` |
61
+ | **review** | Adversarial code review — give and receive with rigor | review notes | heavy | `../review/SKILL.md` |
62
+ | **ship** | Close the branch: PR / merge / cleanup. **Git authorship = Eric** | the merge/PR and archive bundle | light | `../ship/SKILL.md` |
63
+ | **debug** | Root-cause diagnosis: reproduce → isolate → fix → verify | a diagnosis | heavy | `../debug/SKILL.md` |
64
+ | **worktrees** | Isolate feature work in a branch/worktree before executing a plan | an isolated workspace | light | `../worktrees/SKILL.md` |
65
+ | **parallel** | Fan out independent tasks across subagents, gather results | merged results | per-unit | `../parallel/SKILL.md` |
66
+
67
+ > The **Tier** column is the *default* model tier each phase routes to when **per-phase model
68
+ > routing** is enabled — see "Per-phase model routing" below. It is off by default and changes
69
+ > nothing about the chain or the gates; it only decides which model does the work.
66
70
 
67
71
  > If a phase skill above does not yet exist in this repo, the chain still holds — do that phase's work inline following the method here, and skip the broken handoff. Never invent a sibling that is not installed.
68
72
 
@@ -105,6 +109,24 @@ Before dispatching, read `02-DOCS/wiki/harness/user-profile.md` and adapt — ex
105
109
 
106
110
  If there is no profile yet, default to **non-technical + ask the two harness gauging questions** (technical level, accompaniment level) before dispatching, and persist them — that is the harness's job and `sdd` honors it.
107
111
 
112
+ ## Per-phase model routing (opt-in)
113
+
114
+ Different phases reward different models: architecture and adversarial review want the strongest
115
+ one; scaffolding and git plumbing don't. SDD can route each phase to a **tier** — `heavy`
116
+ (deep reasoning), `balanced` (execution), `light` (mechanical) — that resolves to a concrete
117
+ model in `02-DOCS/wiki/sdd/config.yaml` under `models`. The default mapping is the **Tier**
118
+ column above (quality-biased: heavy on constitution/plan/analyze/review/debug, balanced on
119
+ execution, light on ship/worktrees/sdd-init).
120
+
121
+ It is **off by default** (`models.enabled: false`). When the user opts in, each phase applies it
122
+ two ways: **programmatically** — dispatching `Task`/`parallel` subagents on the tier's model
123
+ (real routing, e.g. Claude Code) — and **advisorily** — announcing the recommended switch at the
124
+ phase boundary, gated by the accompaniment dial, for inline work and assistants that can't switch
125
+ programmatically. Never switch unasked, never claim a switch a tool can't make, and skip routing
126
+ on trivial one-line changes. The `sdd` dispatcher itself never routes (staying on the session
127
+ model); `parallel` has no fixed tier (each unit inherits the tier of its work). Full protocol,
128
+ per-assistant mechanism, and the provider→model table: `references/model-routing.md`.
129
+
108
130
  ## Where the artifacts live (and why it matters)
109
131
 
110
132
  Every phase writes under `02-DOCS/wiki/sdd/` so the feature's reasoning outlives the chat:
@@ -158,6 +180,7 @@ Every SDD phase ends with the same parseable block so the dispatcher can chain p
158
180
  "artifact": "path/to/artifact-or-none",
159
181
  "next_recommended": "sdd-init|specify|clarify|plan|tasks|analyze|implement|verify|review|ship",
160
182
  "risk": "low|medium|high",
183
+ "model": { "tier": "heavy|balanced|light", "resolved": "model-id", "routing": "on|off" },
161
184
  "skill_resolution": {
162
185
  "used": [],
163
186
  "missing": [],
@@ -195,6 +218,8 @@ Include current phase, active artifacts, last verdict, completed tasks, next ste
195
218
  | "Run constitution again for this feature." | Constitution is once per project. If it exists, read it as guardrails and move on. |
196
219
  | "Ship now, I'll add Co-Authored-By: Claude." | `ship` enforces Eric-only authorship. No Claude co-author, no generated-with footer. |
197
220
  | "Invoke the `release` phase." | There is no such phase. Never invent a sibling — the chain is the chain. |
221
+ | "Routing sounds useful, I'll switch to the heavy model on my own." | Routing is opt-in (`models.enabled:false`). Don't switch models unless the user turned it on. |
222
+ | "I'll tell them I switched to Opus." (on an assistant with no per-subagent model) | Announce the recommended tier; never claim a switch the tool can't make. Honesty over magic. |
198
223
 
199
224
  ## Start here
200
225
 
@@ -32,6 +32,9 @@ should_trigger:
32
32
  - prompt: "We have an existing repo and no SDD config yet. Start the SDD flow properly before we write a spec."
33
33
  why: "The dispatcher should notice the missing runtime calibration and route to sdd-init before specify on non-trivial work."
34
34
 
35
+ - prompt: "I want planning and review to run on the strongest model but implementation on a cheaper one. Set that up for our SDD flow."
36
+ why: "Per-phase model routing is an SDD concern the dispatcher owns: it should name the tier system (heavy/balanced/light), point at the models block in 02-DOCS/wiki/sdd/config.yaml written by sdd-init, and note it is opt-in — not start coding."
37
+
35
38
  should_not_trigger:
36
39
  - prompt: "Write the spec for the new export-to-CSV feature."
37
40
  route_to: "specify"
@@ -76,3 +79,12 @@ capability:
76
79
  - "Still requires a verification step after the change (the method serves shipping, not ceremony, but evidence before claiming done remains)"
77
80
  - "Does not abandon the method for genuinely risky work — distinguishes trivial from non-trivial and is honest about which this is"
78
81
  - "If git authorship comes up, notes ship enforces Eric-only authorship (no Co-Authored-By Claude, no generated-with footer)"
82
+
83
+ - scenario: "A user asks: 'Can SDD use a cheaper model for the boring phases and the strong one for the hard ones? How do I turn that on?' Show how the dispatcher explains per-phase model routing."
84
+ must_include:
85
+ - "Names the three tiers — heavy (deep reasoning), balanced (execution), light (mechanical) — and that each phase has a default tier"
86
+ - "States the default quality-biased mapping: heavy on constitution/plan/analyze/review/debug, balanced on specify/clarify/tasks/implement/verify, light on ship/worktrees/sdd-init"
87
+ - "Says routing is OFF by default (models.enabled:false) and lives in the models block of 02-DOCS/wiki/sdd/config.yaml, written by sdd-init; turning it on is the user's choice"
88
+ - "Explains the two application layers: programmatic (dispatching Task/parallel subagents on the tier's model, e.g. Claude Code) and advisory (announcing the recommended switch at the phase boundary, gated by the accompaniment dial)"
89
+ - "Is honest about limits: never switches unasked, never claims a model switch an assistant cannot make, and skips routing on trivial one-line changes"
90
+ - "Points to references/model-routing.md for the full protocol, per-assistant mechanism, and provider→model table rather than inventing model names inline"
@@ -0,0 +1,183 @@
1
+ # Per-phase model routing — the protocol
2
+
3
+ *Different SDD phases ask for different kinds of thinking. Architecture and adversarial
4
+ review reward the strongest model; scaffolding and git plumbing don't. This is the protocol
5
+ that lets each phase run on the model its work deserves — cheaper where it's safe, strong
6
+ where it matters — without surprising anyone and without faking a switch a tool can't make.*
7
+
8
+ This file is the **single source of truth** for the behavior. `sdd-init` writes the profile,
9
+ `sdd` summarizes it, and every phase skill points here for the procedure. If the three ever
10
+ disagree, this file wins.
11
+
12
+ ## Why this exists
13
+
14
+ By default every SDD phase runs on whatever model the session happens to be on. That's either
15
+ wasteful (running `ship`'s `git push` on the most expensive model) or risky (running an
16
+ architecture `plan` on the cheapest one). Routing assigns each phase a **tier** and lets the
17
+ phase put its work on the right model. It is **opt-in**: until the user turns it on, nothing
18
+ changes and nothing is announced.
19
+
20
+ ## The three tiers (provider-neutral)
21
+
22
+ Tiers are abstract so the profile survives a provider switch. A phase only ever names a tier;
23
+ the concrete model is resolved from config.
24
+
25
+ | Tier | For | Typical work |
26
+ | --- | --- | --- |
27
+ | **heavy** | deep reasoning | architecture, consistency gates, adversarial review, root-cause diagnosis |
28
+ | **balanced** | execution | writing specs, code, tests, breaking work into tasks, interpreting check output |
29
+ | **light** | mechanical / high-volume / exploration | scaffolding, file sweeps, scans, git plumbing, repo detection |
30
+
31
+ ## The profile (lives in `02-DOCS/wiki/sdd/config.yaml`)
32
+
33
+ `sdd-init` writes this block. It is the canonical shape — keep it byte-for-byte in sync with
34
+ the Config Shape in `../../sdd-init/SKILL.md` and the table in `../SKILL.md`.
35
+
36
+ ```yaml
37
+ models:
38
+ enabled: false # opt-in master switch; false = honor the session model, announce nothing
39
+ provider: anthropic # which provider the tiers below resolve to
40
+ tiers:
41
+ heavy: claude-opus-4-8
42
+ balanced: claude-sonnet-4-6
43
+ light: claude-haiku-4-5-20251001
44
+ phases:
45
+ constitution: heavy
46
+ specify: balanced
47
+ clarify: balanced
48
+ plan: heavy
49
+ tasks: balanced
50
+ analyze: heavy
51
+ implement: balanced
52
+ verify: balanced
53
+ review: heavy
54
+ ship: light
55
+ debug: heavy
56
+ worktrees: light
57
+ sdd-init: light
58
+ overrides: {} # per-phase tier overrides set by the user; preserved across re-calibration
59
+ ```
60
+
61
+ **Master switch.** `enabled: false` (the default) means routing does nothing: honor the
62
+ session model, never announce a switch, never nag. Everything below applies only when
63
+ `enabled: true`.
64
+
65
+ **Two phases are deliberately absent from `phases`:**
66
+
67
+ - `sdd` (the dispatcher) — routing decisions are cheap; it stays on the session model.
68
+ - `parallel` — it has no fixed tier. Each fanned-out subagent inherits the tier of the *kind
69
+ of work* its unit does (an implement-type unit → `balanced`; a scan/research unit → `light`;
70
+ a deep-reasoning unit → `heavy`).
71
+
72
+ ## Default phase → tier mapping (quality-biased) and why
73
+
74
+ | Phase | Tier | Why |
75
+ | --- | --- | --- |
76
+ | constitution | heavy | Sets the project's non-negotiables — highest leverage, worth the best reasoning. |
77
+ | specify | balanced | Drafts the what/why spec through dialogue; not architecture. |
78
+ | clarify | balanced | Ranks and asks the few high-leverage questions; not architecture. |
79
+ | plan | heavy | Architecture, interfaces, data flow, risks — the heaviest cognitive load. |
80
+ | tasks | balanced | Decomposes the plan into verifiable tasks; structured but mechanical. |
81
+ | analyze | heavy | Adversarial consistency gate across constitution ↔ spec ↔ plan ↔ tasks. |
82
+ | implement | balanced | The bulk of TDD execution — cost-sensitive, quality sufficient. |
83
+ | verify | balanced | Runs the checks and interprets failures with judgment. |
84
+ | review | heavy | Adversarial diff reading — where the strongest model pays off most. |
85
+ | ship | light | Close the branch: PR / merge / cleanup — mechanical. |
86
+ | debug | heavy | Root-cause diagnosis — deep reasoning. |
87
+ | worktrees | light | Isolate the workspace (git) — mechanical. |
88
+ | sdd-init | light | Repo detection and calibration — mechanical. |
89
+
90
+ The user owns this. They change a phase by adding it to `models.overrides` (e.g.
91
+ `overrides: { implement: heavy }`), which wins over `phases`.
92
+
93
+ ## How a phase applies the profile (the procedure)
94
+
95
+ Run this at the start of any SDD phase, after reading the accompaniment dial:
96
+
97
+ 1. **Read** `models` from `02-DOCS/wiki/sdd/config.yaml`. If the block is absent or
98
+ `enabled: false` → **do nothing**: honor the session model, say nothing about models.
99
+ 2. **Resolve the tier** for this phase: `models.overrides[phase]` if present, else
100
+ `models.phases[phase]`. (`parallel` resolves a tier per unit instead — see below.)
101
+ 3. **Resolve the model**: `models.tiers[tier]` for the active `models.provider`.
102
+ 4. **Apply, in two layers:**
103
+ - **Programmatic (real routing, where the assistant supports delegation).** When the phase
104
+ delegates work to a subagent (the `Task`/Agent tool) or fans out via `parallel`, set that
105
+ subagent's `model` to the resolved model. This is real routing regardless of the session
106
+ model and is the most reliable application — prefer it.
107
+ - **Advisory (all assistants, inline work).** If the resolved model differs from the current
108
+ session model and the work isn't being delegated, **announce** it at the phase boundary,
109
+ gated by the accompaniment dial (below), and give the switch command for the active
110
+ assistant (mechanism table below). Never block — the human may decline and continue.
111
+ 5. **Skip routing entirely** for a one-line / trivial change, exactly as the SDD skip rule says
112
+ for the rest of the chain. Ceremony serves shipping, not the other way around.
113
+ 6. **Record** the model actually used in the phase's result envelope (`model` field, below).
114
+ Log it to `02-DOCS/wiki/sdd/decisions.md` only when the choice actually mattered (e.g. you
115
+ overrode a tier for a hard plan) — not on every phase.
116
+
117
+ ### Announcement volume by accompaniment dial
118
+
119
+ The dial controls how loudly you announce a switch; it never changes whether routing happens.
120
+
121
+ | Level | On a model switch you… |
122
+ | --- | --- |
123
+ | **L0** "cavernícola" | Switch/dispatch silently. One short line only if the user must act (e.g. "para esta fase conviene opus: `/model opus`"). |
124
+ | **L1** "breve" | One line: which tier/model and the one-word why. |
125
+ | **L2** "explica decisiones" | Name the tier, the model, and why this phase warrants it; give the switch command. |
126
+ | **L3** "acompañamiento total" | Explain the tier system as it applies here, the cost/quality trade-off, and how to override in config. |
127
+
128
+ ## Per-assistant switch mechanism
129
+
130
+ We route programmatically only where the assistant exposes a per-subagent model. Everywhere
131
+ else routing is advisory — announce the tier, the user switches in their tool.
132
+
133
+ | Assistant | Programmatic (delegation) | Advisory (inline) |
134
+ | --- | --- | --- |
135
+ | **Claude Code** | `model` on the `Task`/Agent tool and on `parallel` subagents (`opus`/`sonnet`/`haiku`) | `/model <name>` for the session |
136
+ | **Codex CLI** | — (solo-agent) | model flag / `model` in config |
137
+ | **Cursor** | native subagent model where exposed | model picker in the UI |
138
+ | **Gemini CLI** | — (experimental) | model flag |
139
+ | **Copilot / Windsurf / Cline / others** | — | announce the tier; user switches in the tool's model selector |
140
+
141
+ When the active assistant has **no** way to switch, still announce the recommended tier so the
142
+ human can decide — but **never claim a switch happened**. Honesty over magic.
143
+
144
+ ## Provider → concrete models (repoint the tiers here)
145
+
146
+ To target another provider, change `models.provider` and set `models.tiers` to that row.
147
+
148
+ | provider | heavy | balanced | light |
149
+ | --- | --- | --- | --- |
150
+ | `anthropic` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5-20251001` |
151
+ | `openai` | `gpt-5.1` (or current flagship) | `gpt-5.1-mini` | `gpt-5.1-nano` |
152
+ | `google` | `gemini-2.5-pro` | `gemini-2.5-flash` | `gemini-2.5-flash-lite` |
153
+ | `openrouter` | a strong model slug | a mid model slug | a cheap/free model slug |
154
+
155
+ These are starting points — set them to whatever your account actually has. The tiers are the
156
+ contract; the concrete names are yours to edit.
157
+
158
+ ## Result-envelope `model` field
159
+
160
+ Every phase's result envelope carries the model it ran on, so the chain is auditable:
161
+
162
+ ```json
163
+ {
164
+ "status": "complete",
165
+ "model": { "tier": "heavy", "resolved": "claude-opus-4-8", "routing": "on|off" },
166
+ "...": "rest of the standard envelope"
167
+ }
168
+ ```
169
+
170
+ When routing is off, emit `"model": { "routing": "off" }` (no tier/resolved) — don't invent a
171
+ tier you didn't use.
172
+
173
+ ## Honesty rules / anti-patterns → STOP
174
+
175
+ | Rationalization | Reality |
176
+ | --- | --- |
177
+ | "Routing sounds good, I'll switch models even though the user never enabled it." | It's opt-in. `enabled: false` (and a missing block) means honor the session model and say nothing. |
178
+ | "I'll tell the user I switched to Opus." (on an assistant with no programmatic switch) | You can't switch there. Announce the tier and the command; never claim a switch you didn't make. |
179
+ | "L0 means terse, so I'll skip routing." | L0 changes words, not behavior. Route the same; just announce silently (or only when the user must act). |
180
+ | "It's a one-line typo fix but the phase is `review`, so → heavy." | Skip routing on trivial changes, like the rest of the chain. Don't spin up the expensive model for a copy tweak. |
181
+ | "I'll log the model on every phase." | Log to `decisions.md` only when the model choice mattered. The envelope `model` field already records the routine case. |
182
+ | "config has no `models` block, I'll assume the defaults are active." | A missing block means routing is **off**. Defaults describe what `sdd-init` *writes*, not an implicit on-state. |
183
+ | "Two parallel units, I'll run both on the phase's tier." | `parallel` has no fixed tier. Give each unit the tier of its own work. |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sdd-init
3
- description: "Use when calibrating an existing repo before running the rsc SDD flow: detecting stack, package manager, test runners, lint/type/build commands, monorepo signals, artifact store, execution mode, review budget, strict TDD capability, and project skill registry. Triggers: 'calibrate this repo for SDD', 'run sdd init', 'detect my test runner before implementing', 'set up SDD config', 'prepare this repo for spec-driven development'. NOT first-contact user/workspace bootstrap (init), NOT 01-TOOLS/02-DOCS scaffolding (harness), NOT writing a feature spec (specify)."
3
+ description: "Use when calibrating an existing repo before running the rsc SDD flow: detecting stack, package manager, test runners, lint/type/build commands, monorepo signals, artifact store, execution mode, review budget, strict TDD capability, and project skill registry. Triggers: 'calibrate this repo for SDD', 'run sdd init', 'detect my test runner before implementing', 'set up SDD config', 'prepare this repo for spec-driven development', 'configure per-phase model routing', 'assign models per SDD phase', 'set up cheaper model for implementation'. NOT first-contact user/workspace bootstrap (init), NOT 01-TOOLS/02-DOCS scaffolding (harness), NOT writing a feature spec (specify)."
4
4
  tags: [sdd, init, config, testing, registry]
5
5
  recommends: [sdd, specify, implement, verify]
6
6
  profiles: [core, full]
@@ -38,6 +38,8 @@ Ask only when the answer changes behavior. At L0/L1 infer defaults and show them
38
38
  | `artifact_store` | `02-DOCS/wiki/sdd` | Keep RSC artifacts in `02-DOCS`; do not create an `openspec/` parallel tree. |
39
39
  | `review_budget.line_budget` | `400` | Lower for solo tight review; higher only with explicit approval. |
40
40
  | `delivery_strategy.default` | `ask-on-risk` | `ask-on-risk`, `single-pr`, `autochain`, `exception`. |
41
+ | `models.enabled` | `false` | Per-phase model routing is opt-in. Leave off unless the user asks for it; flipping it on is their call. |
42
+ | `models.provider` | `anthropic` | Which provider the tiers resolve to. Set from the detected assistant when obvious, else `anthropic`. See `../sdd/references/model-routing.md` for other providers. |
41
43
 
42
44
  ## Detection
43
45
 
@@ -48,7 +50,8 @@ Use the repo detector exposed by the CLI code (`detectRepoProfile`) or reproduce
48
50
  - scripts: `test`, `lint`, `typecheck`, `build`;
49
51
  - runners: Vitest, Jest, Playwright, pytest, `go test`, `flutter test`;
50
52
  - monorepo signals;
51
- - recommended apply and verify commands.
53
+ - recommended apply and verify commands;
54
+ - the active assistant/provider when it's obvious from the environment (Claude Code, Codex, Gemini…), to seed `models.provider` — default `anthropic` when unsure. This only sets the *concrete model names*; routing itself stays off until the user opts in.
52
55
 
53
56
  If any runner is detected, set `testing.strict_tdd: true`. Strict TDD means implement phases must do red -> green -> triangulate edge cases -> refactor, with command evidence. If no runner is detected, set it false and record the gap rather than pretending.
54
57
 
@@ -118,9 +121,38 @@ phase_rules:
118
121
  implement: requires analyze pass, strict_tdd when testing.strict_tdd is true
119
122
  verify: requires spec tasks evidence
120
123
  archive: requires verify record and review/ship outcome
124
+ models:
125
+ enabled: false # opt-in master switch; false = honor session model, announce nothing
126
+ provider: anthropic # which provider the tiers below resolve to
127
+ tiers:
128
+ heavy: claude-opus-4-8
129
+ balanced: claude-sonnet-4-6
130
+ light: claude-haiku-4-5-20251001
131
+ phases:
132
+ constitution: heavy
133
+ specify: balanced
134
+ clarify: balanced
135
+ plan: heavy
136
+ tasks: balanced
137
+ analyze: heavy
138
+ implement: balanced
139
+ verify: balanced
140
+ review: heavy
141
+ ship: light
142
+ debug: heavy
143
+ worktrees: light
144
+ sdd-init: light
145
+ overrides: {} # per-phase tier overrides set by the user
121
146
  ```
122
147
 
123
- Preserve user edits if the file exists: update detected facts and leave comments/custom policy fields intact when possible. If preservation is risky, write a proposed replacement next to it as `config.proposed.yaml` and ask.
148
+ The `models` block is the **per-phase model routing** profile: each phase declares a tier
149
+ (`heavy`/`balanced`/`light`) and the tiers resolve to concrete models for `provider`. It ships
150
+ **off** (`enabled: false`) — write it so the user can opt in, never switch models behind their
151
+ back. The full protocol (how phases apply it, the per-assistant switch mechanism, the
152
+ provider→model table) lives in `../sdd/references/model-routing.md`. Keep this block byte-for-byte
153
+ in sync with that reference.
154
+
155
+ Preserve user edits if the file exists: update detected facts and leave comments/custom policy fields intact when possible. **Never flip `models.enabled` or drop `models.overrides` on re-calibration** — those are user choices; preserve them verbatim and only refresh `tiers`/`provider` if the user asks. If preservation is risky, write a proposed replacement next to it as `config.proposed.yaml` and ask.
124
156
 
125
157
  ## Result Envelope
126
158
 
@@ -139,7 +171,8 @@ End with the standard SDD result envelope:
139
171
  "fallback": [],
140
172
  "compact_rules": [
141
173
  "Read config.yaml before choosing commands.",
142
- "Use .rsc/skill-registry.json as the cheap skill index."
174
+ "Use .rsc/skill-registry.json as the cheap skill index.",
175
+ "Per-phase model routing ships off (models.enabled:false); never switch models unasked."
143
176
  ]
144
177
  },
145
178
  "evidence": ["npx @ericrisco/rsc registry refresh", "detected test commands recorded"]
@@ -186,6 +186,10 @@ The commit is the durable record. Make it describe the change and tie it to the
186
186
  - **Body:** *why*, not a restatement of the diff. Reference the spec slug and any decision logged in `decisions.md`.
187
187
  - **Footer:** issue/PR refs only. **No `Co-Authored-By` for any AI. No "generated with" line.** This is where the violation usually sneaks in — leave the footer clean.
188
188
 
189
+ ## Model tier — `light` (opt-in routing)
190
+
191
+ This phase's default model tier is **`light`** — closing the branch (PR / merge / cleanup) is mechanical. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change. The Eric-only authorship rule is independent of the model and never relaxes.
192
+
189
193
  ## Accompaniment dial (L0..L3)
190
194
 
191
195
  Read the level from `02-DOCS/wiki/harness/user-profile.md`. It changes the narration, **never** the safety checklist or the authorship rule.
@@ -28,6 +28,10 @@ You are not slowing them down; you make the intent reviewable *before* code exis
28
28
 
29
29
  If you cannot describe a requirement without naming the technology, you have found a real question — record it as a point to clarify, do not guess the answer.
30
30
 
31
+ ## Model tier — `balanced` (opt-in routing)
32
+
33
+ This phase's default model tier is **`balanced`** — it drafts the what/why spec through dialogue, not architecture. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
34
+
31
35
  ## Read the room first (accompaniment dial)
32
36
 
33
37
  Before asking anything, read `02-DOCS/wiki/harness/user-profile.md` for the technical level and accompaniment level, and adapt:
@@ -25,6 +25,10 @@ further questions.
25
25
  This is a process skill. It writes no runtime code. It produces one artifact: an
26
26
  ordered, checkable task list appended to the plan it was built from.
27
27
 
28
+ ## Model tier — `balanced` (opt-in routing)
29
+
30
+ This phase's default model tier is **`balanced`** — it decomposes an approved plan into verifiable tasks: structured work, not architecture. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
31
+
28
32
  ## Read the harness profile first
29
33
 
30
34
  Before producing anything, read `02-DOCS/wiki/harness/user-profile.md` for the
@@ -31,6 +31,10 @@ Do NOT use when:
31
31
  - You're reading the diff adversarially for design/correctness smells a test can't catch → that's `review`.
32
32
  - There is no spec or task list to verify against → you're earlier in the chain; go to `specify`/`plan`/`tasks` first.
33
33
 
34
+ ## Model tier — `balanced` (opt-in routing)
35
+
36
+ This phase's default model tier is **`balanced`** — it runs the checks and interprets failures with judgment. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
37
+
34
38
  ## Read the room first (accompaniment dial)
35
39
 
36
40
  Before running anything, read `02-DOCS/wiki/harness/user-profile.md` for the technical + accompaniment level and adapt:
@@ -125,6 +125,10 @@ level) before handing to `implement`.
125
125
  The native tool is preferred because it owns the session-switch and the exit lifecycle for you. The
126
126
  git path is the universal fallback and is exactly what the native tool does under the hood.
127
127
 
128
+ ## Model tier — `light` (opt-in routing)
129
+
130
+ This phase's default model tier is **`light`** — isolating the workspace is mechanical git work. Routing is **off** unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`. When on: resolve this phase's tier (`models.overrides` wins over `models.phases`), map it to a model via `models.tiers`, and apply per `../sdd/references/model-routing.md` — announce the switch per the accompaniment dial when it differs from the session model, and dispatch any `Task`/`parallel` subagents on that model. Routing off or no profile → honor the session model silently. Never fake a switch a tool can't make; skip routing on a one-line change.
131
+
128
132
  ## Adapting to the dial
129
133
 
130
134
  Read `02-DOCS/wiki/harness/user-profile.md` and match your volume — the isolation is identical at