super-backlog 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,14 +19,41 @@ Superpowers and Backlog.md are strong on their own, but nothing wires them toget
19
19
 
20
20
  ## Quickstart
21
21
 
22
+ Install super-backlog into the current project with the official installer for your platform:
23
+
24
+ **Windows (PowerShell)** — bypasses Execution Policy automatically:
25
+
26
+ ```powershell
27
+ irm https://raw.githubusercontent.com/adam-s-k-i/super-backlog/main/scripts/install.ps1 | iex
28
+ ```
29
+
30
+ **macOS / Linux**:
31
+
22
32
  ```bash
23
- npx super-backlog init # install everything into the current project
24
- npm run board # open the Backlog.md kanban board
25
- sbl dashboard --serve # live Project Dashboard on http://localhost:6428
26
- # (or: npx super-backlog dashboard --serve)
33
+ curl -fsSL https://raw.githubusercontent.com/adam-s-k-i/super-backlog/main/scripts/install.sh | bash
27
34
  ```
28
35
 
29
- On Windows PowerShell, `npx super-backlog init` may fail with an execution-policy error before the CLI starts. Run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once in PowerShell, or run `sbl doctor` for the exact policy and fix.
36
+ Both installers check Node.js and npm, install `super-backlog`, and run `sbl init` without using `npx`, so they work even when `npx` is blocked by PowerShell's Execution Policy.
37
+
38
+ **Already have Node and npm?** You can also install manually without `npx`:
39
+
40
+ ```bash
41
+ # Global install
42
+ npm install -g super-backlog
43
+ sbl init
44
+ sbl init --models
45
+
46
+ # Local install
47
+ npm install super-backlog
48
+ node ./node_modules/super-backlog/dist/cli.js init
49
+ ```
50
+
51
+ After installation:
52
+
53
+ ```bash
54
+ npm run board # open the Backlog.md kanban board
55
+ sbl dashboard --serve # live Project Dashboard on http://localhost:6428
56
+ ```
30
57
 
31
58
  `init` is idempotent — safe to re-run any time; re-running with a newer kit version is the upgrade path for all injected files.
32
59
 
@@ -47,7 +74,27 @@ On Windows PowerShell, `npx super-backlog init` may fail with an execution-polic
47
74
  | `.git/hooks/post-commit` | dashboard freshness block — regenerates `dashboard.html` after commits that touch `backlog/` (default; opt out with `--no-refresh-hook`) | appended marker block |
48
75
  | `.git/hooks/pre-commit` | integrity guard hook — only with `--guard` (opt-in) | appended marker block |
49
76
 
50
- Commands: `sbl init` · `sbl doctor` · `sbl uninstall [--with-backlog]` · `sbl update` · `sbl dashboard [--serve] [--port <n>] [--no-open] [--out <file>]`. See `sbl help` for every flag.
77
+ Commands: `sbl init` · `sbl models` · `sbl doctor` · `sbl uninstall [--with-backlog]` · `sbl update` · `sbl dashboard [--serve] [--port <n>] [--no-open] [--out <file>]`. See `sbl help` for every flag.
78
+
79
+ ## Model router (opt-in)
80
+
81
+ `sbl init --models` adds an optional model router that routes simple work to cheaper model tiers while keeping your main model for complex tasks.
82
+
83
+ ```bash
84
+ sbl init --models # install router + harness adapters
85
+ sbl models enable # turn routing on
86
+ sbl models disable # turn routing off
87
+ sbl models show # inspect current config
88
+ sbl models discover # discover available OpenCode models
89
+ ```
90
+
91
+ When enabled:
92
+
93
+ - **OpenCode** — the plugin `sbl-model-router.js` rewrites `chat.params` for the `sbl-worker` (workhorse) and `sbl-worker-cheap` / `explore` (budget) agents.
94
+ - **Claude Code** — agent files carry a `model:` placeholder that is updated by a `SessionStart` hook based on your current main model.
95
+ - **Dashboard** — `sbl dashboard --serve` exposes `/api/models` and `/api/models/discover`.
96
+
97
+ The router is fully owned by super-backlog and removed by `sbl uninstall`. See the [model router design](docs/superpowers/specs/2026-08-26-sbl-model-router-design.md) for details.
51
98
 
52
99
  ## Project Dashboard
53
100
 
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import process from 'node:process';
5
5
  import { runDashboard } from './commands/dashboard.js';
6
6
  import { runDoctor } from './commands/doctor.js';
7
7
  import { runInit } from './commands/init.js';
8
+ import { runModels } from './commands/models.js';
8
9
  import { runUninstall } from './commands/uninstall.js';
9
10
  import { runUpdate } from './commands/update.js';
10
11
  import { assertNode20, KIT_VERSION } from './lib/version.js';
@@ -17,12 +18,15 @@ Commands:
17
18
  uninstall Remove kit-managed files (project data kept unless --with-backlog)
18
19
  update Refresh kit-managed files and report upstream versions
19
20
  dashboard Generate the single-file project dashboard (--serve for live mode)
21
+ models Manage the model router (show, enable, disable, discover)
20
22
  doctor Check the environment (node, PowerShell policy, backlog CLI)
21
23
 
22
24
  init options:
23
25
  --pm <auto|npm|pnpm|bun|skip> Package manager to use (default: auto)
24
26
  --harness <opencode|claude> Target harness; repeatable or comma-separated (default: both)
25
27
  --guard Install the integrity pre-commit hook (opt-in)
28
+ --models Install the model router config during init (opt-in)
29
+ --no-models Explicitly opt out of the model router
26
30
  --no-dashboard Skip generating the project dashboard
27
31
  --no-refresh-hook Skip the post-commit dashboard freshness hook
28
32
  --dry-run Show what would be done without writing anything
@@ -67,6 +71,8 @@ async function main(argv) {
67
71
  pm: { type: 'string' },
68
72
  harness: { type: 'string', multiple: true },
69
73
  guard: { type: 'boolean' },
74
+ models: { type: 'boolean' },
75
+ 'no-models': { type: 'boolean' },
70
76
  'no-dashboard': { type: 'boolean' },
71
77
  'no-refresh-hook': { type: 'boolean' },
72
78
  'dry-run': { type: 'boolean' },
@@ -111,6 +117,13 @@ async function main(argv) {
111
117
  positionals: parsed.positionals,
112
118
  });
113
119
  }
120
+ case 'models': {
121
+ const parsed = parseArgs({ args: rest, allowPositionals: true, options: {} });
122
+ return await runModels(process.cwd(), {
123
+ values: parsed.values,
124
+ positionals: parsed.positionals,
125
+ });
126
+ }
114
127
  case 'doctor':
115
128
  return runDoctor(process.cwd());
116
129
  default:
@@ -23,6 +23,8 @@ function describeAction(action) {
23
23
  return 'write-claude-pointer CLAUDE.md';
24
24
  case 'copy-skills':
25
25
  return 'copy-skills (.opencode/skill + .claude/skills)';
26
+ case 'install-model-router':
27
+ return 'install-model-router .super-backlog/models.json';
26
28
  case 'install-guard-hook':
27
29
  return 'install-guard-hook .git/hooks/pre-commit';
28
30
  case 'install-refresh-hook':
@@ -74,6 +76,7 @@ export async function runInit(cwd, args) {
74
76
  const dashboard = args.values['no-dashboard'] !== true;
75
77
  const refreshHook = args.values['no-refresh-hook'] !== true; // default on, opt-out flag
76
78
  const dryRun = args.values['dry-run'] === true;
79
+ const models = args.values.models === true ? true : args.values['no-models'] === true ? false : undefined;
77
80
  const projectName = args.positionals[0] ?? basename(resolve(cwd));
78
81
  let opencodeConfig;
79
82
  const opencodePath = join(cwd, 'opencode.json');
@@ -95,7 +98,7 @@ export async function runInit(cwd, args) {
95
98
  opencodeConfig,
96
99
  pkgExists: existsSync(join(cwd, 'package.json')),
97
100
  };
98
- const opts = { projectName, harnesses, pm, guard, dashboard, refreshHook, skipInstall: false };
101
+ const opts = { projectName, harnesses, pm, guard, dashboard, refreshHook, skipInstall: false, models };
99
102
  const plan = planInit(state, opts, KIT_VERSION);
100
103
  if (dryRun) {
101
104
  console.log(`dry-run for "${projectName}": ${plan.actions.length} action(s) planned, nothing written`);
@@ -0,0 +1,34 @@
1
+ // src/commands/models.ts
2
+ import { loadConfig } from '../models/config.js';
3
+ import { discoverModels } from '../models/discovery.js';
4
+ import { writeRouterConfig } from '../models/install.js';
5
+ export async function runModels(cwd, args) {
6
+ const sub = args.positionals[0] ?? 'show';
7
+ switch (sub) {
8
+ case 'show': {
9
+ const cfg = loadConfig(cwd);
10
+ console.log(JSON.stringify(cfg, null, 2));
11
+ return 0;
12
+ }
13
+ case 'enable':
14
+ writeRouterConfig(cwd, true);
15
+ console.log('model router enabled');
16
+ return 0;
17
+ case 'disable':
18
+ writeRouterConfig(cwd, false);
19
+ console.log('model router disabled');
20
+ return 0;
21
+ case 'discover': {
22
+ const resolved = await discoverModels(cwd);
23
+ if (!resolved) {
24
+ console.error('discovery failed');
25
+ return 1;
26
+ }
27
+ console.log(JSON.stringify(resolved, null, 2));
28
+ return 0;
29
+ }
30
+ default:
31
+ console.error(`unknown models subcommand: ${sub}`);
32
+ return 1;
33
+ }
34
+ }
@@ -8,6 +8,7 @@ import { stripOwned } from '../lib/markers.js';
8
8
  import { PLUGIN_SPEC } from '../lib/opencode.js';
9
9
  import { isOwnedSkillFile } from '../lib/ownership.js';
10
10
  import { WANTED_SCRIPTS } from '../lib/pkgjson.js';
11
+ import { uninstallModelRouter } from '../models/uninstall.js';
11
12
  const OWNED_SKILL_DIRS = [
12
13
  '.opencode/skill/spec-to-backlog',
13
14
  '.opencode/skill/backlog-status-report',
@@ -265,6 +266,7 @@ export function runUninstall(cwd, args) {
265
266
  label: 'backlog/ (project task data preserved - pass --with-backlog to delete)',
266
267
  });
267
268
  }
269
+ uninstallModelRouter(cwd, report);
268
270
  console.log('super-backlog uninstall');
269
271
  for (const line of report)
270
272
  console.log(`${line.verdict}: ${line.label}`);
@@ -5,6 +5,7 @@ import { readFile } from 'node:fs/promises';
5
5
  import { createServer } from 'node:http';
6
6
  import { isAbsolute, join } from 'node:path';
7
7
  import process from 'node:process';
8
+ import { createModelApiHandler } from '../models/dashboard-api.js';
8
9
  export const DASHBOARD_PORT = 6428;
9
10
  export function recursiveWatchSupported(platform, nodeVersion) {
10
11
  // Node 24 on Windows triggers a libuv assertion in recursive fs.watch:
@@ -70,7 +71,12 @@ export async function startServeServer(cwd, opts = {}) {
70
71
  else {
71
72
  console.warn('warning: live reload is disabled because Node 24+ on Windows cannot reliably watch directories recursively (libuv fs-event bug); use Node 22 or Linux/macOS for --serve');
72
73
  }
74
+ const modelApi = createModelApiHandler();
73
75
  const server = createServer((req, res) => {
76
+ if (req.url?.startsWith('/api/')) {
77
+ void modelApi(req, res);
78
+ return;
79
+ }
74
80
  const url = req.url ?? '/';
75
81
  const method = req.method ?? 'GET';
76
82
  if (method !== 'GET' || !(url === '/' || url === '/index.html')) {
@@ -8,6 +8,7 @@ import { installGuardHook, installRefreshHook } from '../lib/hooks.js';
8
8
  import { injectBlock } from '../lib/markers.js';
9
9
  import { applyPluginEntry } from '../lib/opencode.js';
10
10
  import { OwnershipError, renderSkill } from '../lib/ownership.js';
11
+ import { writeRouterConfig } from '../models/install.js';
11
12
  import { addDevDependencies, mergeScripts, WANTED_DEVS, WANTED_SCRIPTS, } from '../lib/pkgjson.js';
12
13
  import { installCmdsFor } from '../lib/pm.js';
13
14
  import { resolveBacklogBin, runCapture } from '../lib/run.js';
@@ -216,6 +217,17 @@ export async function executeActions(cwd, actions, ctx) {
216
217
  case 'generate-dashboard':
217
218
  (await applyGenerateDashboard(cwd, warnings)) ? applied++ : skipped++;
218
219
  break;
220
+ case 'install-model-router': {
221
+ const { installOpenCodeAdapter } = await import('../models/opencode.js');
222
+ const { installClaudeAdapter } = await import('../models/claude.js');
223
+ writeRouterConfig(cwd, action.enabled);
224
+ if (action.enabled) {
225
+ installOpenCodeAdapter(cwd);
226
+ installClaudeAdapter(cwd);
227
+ }
228
+ applied++;
229
+ break;
230
+ }
219
231
  case 'write':
220
232
  atomicWrite(join(cwd, action.path), action.contents);
221
233
  applied++;
@@ -56,5 +56,7 @@ export function planInit(state, opts, _version) {
56
56
  actions.push({ kind: 'install-refresh-hook' });
57
57
  if (opts.dashboard)
58
58
  actions.push({ kind: 'generate-dashboard' });
59
+ if (opts.models === true)
60
+ actions.push({ kind: 'install-model-router', enabled: true });
59
61
  return { actions, warnings };
60
62
  }
@@ -0,0 +1,61 @@
1
+ // src/models/claude.ts
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { atomicWrite } from '../lib/atomic.js';
6
+ import { loadConfig } from './config.js';
7
+ import { resolveTier } from './resolve.js';
8
+ const MODEL_RE = /^model:\s*.+$/m;
9
+ export function syncClaudeAgents(cwd, mainModel) {
10
+ const cfg = loadConfig(cwd);
11
+ if (!cfg.enabled)
12
+ return;
13
+ const workhorse = mainModel ? resolveTier(mainModel, 'workhorse', cfg) : cfg.resolved.workhorse || null;
14
+ const budget = mainModel ? resolveTier(mainModel, 'budget', cfg) : cfg.resolved.budget || null;
15
+ updateAgentFile(cwd, 'sbl-worker.md', workhorse);
16
+ updateAgentFile(cwd, 'sbl-worker-cheap.md', budget);
17
+ }
18
+ export function updateAgentFile(cwd, file, model) {
19
+ const path = join(cwd, '.claude', 'agents', file);
20
+ if (!existsSync(path))
21
+ return;
22
+ let content = readFileSync(path, 'utf8');
23
+ const replacement = `model: ${model ?? 'inherit'}`;
24
+ if (MODEL_RE.test(content)) {
25
+ content = content.replace(MODEL_RE, replacement);
26
+ }
27
+ else {
28
+ content = content.replace(/^---$/m, `---\n${replacement}`);
29
+ }
30
+ atomicWrite(path, content);
31
+ }
32
+ const __dirname = dirname(fileURLToPath(import.meta.url));
33
+ export function installClaudeAdapter(cwd) {
34
+ const workerPath = join(cwd, '.claude', 'agents', 'sbl-worker.md');
35
+ const cheapPath = join(cwd, '.claude', 'agents', 'sbl-worker-cheap.md');
36
+ atomicWrite(workerPath, readFileSync(join(__dirname, '../templates/claude-agent-sbl-worker.md'), 'utf8'));
37
+ atomicWrite(cheapPath, readFileSync(join(__dirname, '../templates/claude-agent-sbl-worker-cheap.md'), 'utf8'));
38
+ installSettingsHook(cwd);
39
+ }
40
+ function installSettingsHook(cwd) {
41
+ const path = join(cwd, '.claude', 'settings.json');
42
+ const existing = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
43
+ const hook = {
44
+ hooks: {
45
+ SessionStart: [
46
+ {
47
+ matcher: '*',
48
+ hooks: [
49
+ {
50
+ type: 'command',
51
+ command: 'node',
52
+ args: ['node_modules/super-backlog/dist/templates/cc-session-hook.js'],
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ },
58
+ };
59
+ const next = { ...existing, hooks: { ...existing.hooks, ...hook.hooks } };
60
+ atomicWrite(path, `${JSON.stringify(next, null, 2)}\n`);
61
+ }
@@ -0,0 +1,30 @@
1
+ import process from 'node:process';
2
+ import { loadConfig } from './config.js';
3
+ import { discoverModels } from './discovery.js';
4
+ function currentCwd() {
5
+ try {
6
+ return process.cwd();
7
+ }
8
+ catch {
9
+ return '.';
10
+ }
11
+ }
12
+ export function createModelApiHandler() {
13
+ return async (req, res) => {
14
+ const url = req.url ?? '/';
15
+ const method = req.method ?? 'GET';
16
+ if (method === 'GET' && url === '/api/models') {
17
+ res.writeHead(200, { 'content-type': 'application/json' });
18
+ res.end(JSON.stringify({ config: loadConfig(currentCwd()), status: 'ok' }));
19
+ return;
20
+ }
21
+ if (method === 'POST' && url === '/api/models/discover') {
22
+ const result = await discoverModels(currentCwd());
23
+ res.writeHead(200, { 'content-type': 'application/json' });
24
+ res.end(JSON.stringify(result ?? { error: 'discovery failed' }));
25
+ return;
26
+ }
27
+ res.writeHead(404, { 'content-type': 'text/plain' });
28
+ res.end('not found');
29
+ };
30
+ }
@@ -0,0 +1,66 @@
1
+ import { runCapture } from '../lib/run.js';
2
+ export function parseOpenCodeModels(output) {
3
+ return output
4
+ .split('\n')
5
+ .map((s) => s.trim())
6
+ .filter((s) => s.length > 0 && s.includes('/'));
7
+ }
8
+ function scoreForTier(modelId, tier) {
9
+ const lower = modelId.toLowerCase();
10
+ // Prefer flagship-ish names for workhorse, avoid 'nano/lite' for workhorse
11
+ if (tier === 'workhorse') {
12
+ if (lower.includes('opus'))
13
+ return 100;
14
+ if (lower.includes('sonnet'))
15
+ return 90;
16
+ if (lower.includes('k2.7-code') || lower.includes('k2.6-code'))
17
+ return 85;
18
+ if (lower.includes('gpt-5.1') || lower.includes('gpt-5.2') || lower.includes('k3'))
19
+ return 80;
20
+ if (lower.includes('gemini-3.1') || lower.includes('gemini-3.5') || lower.includes('gemini-3.6'))
21
+ return 70;
22
+ if (lower.includes('k2.7') || lower.includes('k2.6'))
23
+ return 60;
24
+ if (lower.includes('grok-4.6'))
25
+ return 55;
26
+ if (lower.includes('grok-4.5'))
27
+ return 50;
28
+ return 10;
29
+ }
30
+ // Budget tier: prefer nano/lite/flash names
31
+ if (lower.includes('nano') || lower.includes('lite'))
32
+ return 100;
33
+ if (lower.includes('flash'))
34
+ return 90;
35
+ if (lower.includes('haiku'))
36
+ return 85;
37
+ if (lower.includes('k2.5'))
38
+ return 80;
39
+ if (lower.includes('grok-4.3'))
40
+ return 70;
41
+ return 10;
42
+ }
43
+ export function rankTiers(available) {
44
+ const workhorse = [...available].sort((a, b) => scoreForTier(b, 'workhorse') - scoreForTier(a, 'workhorse'))[0] || '';
45
+ const budget = [...available].sort((a, b) => scoreForTier(b, 'budget') - scoreForTier(a, 'budget'))[0] || '';
46
+ return { workhorse, budget };
47
+ }
48
+ export async function discoverModels(cwd) {
49
+ try {
50
+ const result = runCapture('opencode', ['models'], cwd);
51
+ if (result.status !== 0)
52
+ return null;
53
+ const available = parseOpenCodeModels(result.stdout);
54
+ if (available.length === 0)
55
+ return null;
56
+ const ranked = rankTiers(available);
57
+ return {
58
+ discoveredAt: new Date().toISOString(),
59
+ workhorse: ranked.workhorse,
60
+ budget: ranked.budget,
61
+ };
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
@@ -0,0 +1,18 @@
1
+ // src/models/install.ts
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { atomicWrite } from '../lib/atomic.js';
5
+ import { loadConfig } from './config.js';
6
+ const CONFIG_DIR = '.super-backlog';
7
+ const CONFIG_FILE = 'models.json';
8
+ export function writeRouterConfig(cwd, enabled) {
9
+ const dir = join(cwd, CONFIG_DIR);
10
+ const path = join(dir, CONFIG_FILE);
11
+ if (!existsSync(dir)) {
12
+ mkdirSync(dir, { recursive: true });
13
+ }
14
+ const current = loadConfig(cwd);
15
+ const next = { ...current, enabled };
16
+ atomicWrite(path, `${JSON.stringify(next, null, 2)}\n`);
17
+ return true;
18
+ }
@@ -0,0 +1,14 @@
1
+ // src/models/opencode.ts
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { atomicWrite } from '../lib/atomic.js';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ export function installOpenCodeAdapter(cwd) {
8
+ const pluginPath = join(cwd, '.opencode', 'plugins', 'sbl-model-router.js');
9
+ const workerPath = join(cwd, '.opencode', 'agents', 'sbl-worker.md');
10
+ const cheapPath = join(cwd, '.opencode', 'agents', 'sbl-worker-cheap.md');
11
+ atomicWrite(pluginPath, readFileSync(join(__dirname, '../templates/model-router-plugin.js'), 'utf8'));
12
+ atomicWrite(workerPath, readFileSync(join(__dirname, '../templates/agent-sbl-worker.md'), 'utf8'));
13
+ atomicWrite(cheapPath, readFileSync(join(__dirname, '../templates/agent-sbl-worker-cheap.md'), 'utf8'));
14
+ }
@@ -0,0 +1,91 @@
1
+ // src/models/uninstall.ts
2
+ import { existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { atomicWrite } from '../lib/atomic.js';
5
+ const ROUTER_FILES = [
6
+ '.super-backlog/models.json',
7
+ '.opencode/plugins/sbl-model-router.js',
8
+ '.opencode/agents/sbl-worker.md',
9
+ '.opencode/agents/sbl-worker-cheap.md',
10
+ '.claude/agents/sbl-worker.md',
11
+ '.claude/agents/sbl-worker-cheap.md',
12
+ ];
13
+ export function uninstallModelRouter(cwd, report) {
14
+ for (const rel of ROUTER_FILES) {
15
+ const abs = join(cwd, rel);
16
+ if (!existsSync(abs)) {
17
+ report.push({ verdict: 'skipped', label: `${rel} (not found)` });
18
+ continue;
19
+ }
20
+ rmSync(abs, { force: true });
21
+ report.push({ verdict: 'removed', label: rel });
22
+ }
23
+ // Remove .super-backlog directory if it is now empty
24
+ const dir = join(cwd, '.super-backlog');
25
+ if (existsSync(dir)) {
26
+ try {
27
+ const remaining = readdirSync(dir);
28
+ if (remaining.length === 0) {
29
+ rmSync(dir, { recursive: true, force: true });
30
+ report.push({ verdict: 'removed', label: '.super-backlog/' });
31
+ }
32
+ else {
33
+ report.push({ verdict: 'kept', label: '.super-backlog/ (contains other files)' });
34
+ }
35
+ }
36
+ catch {
37
+ // ignore race conditions
38
+ }
39
+ }
40
+ uninstallClaudeSettingsHook(cwd, report);
41
+ }
42
+ function uninstallClaudeSettingsHook(cwd, report) {
43
+ const path = join(cwd, '.claude', 'settings.json');
44
+ if (!existsSync(path)) {
45
+ report.push({ verdict: 'skipped', label: '.claude/settings.json model-router hook (not found)' });
46
+ return;
47
+ }
48
+ let settings;
49
+ try {
50
+ settings = JSON.parse(readFileSync(path, 'utf8'));
51
+ }
52
+ catch {
53
+ report.push({ verdict: 'skipped', label: '.claude/settings.json (not valid JSON)' });
54
+ return;
55
+ }
56
+ const before = JSON.stringify(settings);
57
+ const sessionStart = settings.hooks?.SessionStart;
58
+ if (!Array.isArray(sessionStart) || sessionStart.length === 0) {
59
+ report.push({ verdict: 'skipped', label: '.claude/settings.json SessionStart hook (none)' });
60
+ return;
61
+ }
62
+ const filtered = sessionStart.filter((entry) => {
63
+ const hooks = entry.hooks;
64
+ if (!Array.isArray(hooks))
65
+ return true;
66
+ return !hooks.some((h) => typeof h === 'object' &&
67
+ h !== null &&
68
+ h.type === 'command' &&
69
+ Array.isArray(h.args) &&
70
+ String(h.args?.[0] ?? '').includes('cc-session-hook.js'));
71
+ });
72
+ if (filtered.length === sessionStart.length) {
73
+ report.push({ verdict: 'skipped', label: '.claude/settings.json SessionStart hook (none owned)' });
74
+ return;
75
+ }
76
+ if (filtered.length === 0) {
77
+ delete settings.hooks.SessionStart;
78
+ if (Object.keys(settings.hooks ?? {}).length === 0)
79
+ delete settings.hooks;
80
+ }
81
+ else {
82
+ settings.hooks.SessionStart = filtered;
83
+ }
84
+ if (JSON.stringify(settings) !== before) {
85
+ atomicWrite(path, `${JSON.stringify(settings, null, 2)}\n`);
86
+ report.push({ verdict: 'removed', label: '.claude/settings.json SessionStart hook' });
87
+ }
88
+ else {
89
+ report.push({ verdict: 'skipped', label: '.claude/settings.json SessionStart hook (none owned)' });
90
+ }
91
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: sbl-worker-cheap
3
+ description: Runs spikes, trivial edits, and mechanical searches. Use for fast, cheap work.
4
+ mode: subagent
5
+ ---
6
+ Complete the requested lightweight task as cheaply and correctly as possible.
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: sbl-worker
3
+ description: Runs bounded implementation tasks. Use via the task tool for well-scoped code changes.
4
+ mode: subagent
5
+ ---
6
+ Implement the requested bounded change following project conventions and the existing codebase patterns.
@@ -0,0 +1,26 @@
1
+ // src/templates/cc-session-hook.js
2
+ import { loadConfig } from 'super-backlog/dist/models/config.js';
3
+ import { resolveTier } from 'super-backlog/dist/models/resolve.js';
4
+ import { updateAgentFile } from 'super-backlog/dist/models/claude.js';
5
+
6
+ async function main() {
7
+ try {
8
+ let input = '';
9
+ process.stdin.on('data', (d) => { input += d; });
10
+ await new Promise((resolve) => process.stdin.on('end', resolve));
11
+ let event = {};
12
+ try { event = JSON.parse(input); } catch { /* ignore */ }
13
+ const cwd = process.cwd();
14
+ const cfg = loadConfig(cwd);
15
+ if (!cfg.enabled) process.exit(0);
16
+ const mainModel = event.model;
17
+ if (!mainModel || typeof mainModel !== 'string') process.exit(0);
18
+ updateAgentFile(cwd, 'sbl-worker.md', resolveTier(mainModel, 'workhorse', cfg));
19
+ updateAgentFile(cwd, 'sbl-worker-cheap.md', resolveTier(mainModel, 'budget', cfg));
20
+ } catch {
21
+ // degrade silently so a broken router never blocks Claude Code startup
22
+ }
23
+ process.exit(0);
24
+ }
25
+
26
+ main().catch(() => process.exit(0));
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: sbl-worker-cheap
3
+ description: Runs spikes, trivial edits, and mechanical searches. Use for fast, cheap work.
4
+ model: TIER_PLACEHOLDER
5
+ ---
6
+ Complete the requested lightweight task as cheaply and correctly as possible.
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: sbl-worker
3
+ description: Runs bounded implementation tasks. Use via the Agent tool for well-scoped code changes.
4
+ model: TIER_PLACEHOLDER
5
+ ---
6
+ Implement the requested bounded change following project conventions and the existing codebase patterns.
@@ -0,0 +1,27 @@
1
+ // src/templates/model-router-plugin.js
2
+ module.exports = async () => {
3
+ const { loadConfig } = await import('super-backlog/dist/models/config.js');
4
+ const { resolveTier } = await import('super-backlog/dist/models/resolve.js');
5
+
6
+ const TIER_BY_AGENT = {
7
+ 'sbl-worker': 'workhorse',
8
+ 'sbl-worker-cheap': 'budget',
9
+ 'explore': 'budget',
10
+ };
11
+
12
+ return {
13
+ 'chat.params': async (input, output) => {
14
+ const cfg = loadConfig(input.cwd ?? process.cwd());
15
+ if (!cfg.enabled) return;
16
+ const agent = input.agent?.name;
17
+ const tier = TIER_BY_AGENT[agent];
18
+ if (!tier) return;
19
+ const mainModel = input.model;
20
+ if (!mainModel || typeof mainModel !== 'string') return;
21
+ const target = resolveTier(mainModel, tier, cfg);
22
+ if (!target) return;
23
+ output.params = output.params || {};
24
+ output.params.model = target;
25
+ },
26
+ };
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-backlog",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "One command to equip any project with Backlog.md + Superpowers, plus a Project Dashboard.",
5
5
  "license": "MIT",
6
6
  "repository": {