phewsh 0.14.5 → 0.14.6

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
@@ -31,14 +31,14 @@ phewsh ai providers # see what's installed
31
31
  API keys (OpenRouter, Anthropic, Groq, …) and PHEWSH pooled credits remain
32
32
  available as alternatives — `phewsh login --set-key`.
33
33
 
34
- ## Two packages, one system
35
-
36
- - **`phewsh`** (this package) — the CLI: intent authoring, sync, bridges,
37
- receipts, dispatch.
38
- - **`phewsh-mcp-server`** the MCP server that `phewsh mcp setup` wires into
39
- Claude Code / Cursor / any MCP client. It gives an *interactive* agent
40
- session your project's briefing, task queue, and enforcement gate over
41
- stdio, and shares state with the bridges (`~/.phewsh/`).
34
+ ## One package, one system
35
+
36
+ Everything ships in `phewsh` — the CLI (intent authoring, sync, bridges,
37
+ receipts, dispatch) *and* the MCP server. `phewsh mcp setup` wires the
38
+ bundled server into Claude Code / Cursor / any MCP client via
39
+ `phewsh mcp serve --stdio`: an *interactive* agent session gets your
40
+ project's briefing, task queue, and enforcement gate over stdio, and shares
41
+ state with the bridges (`~/.phewsh/`). No second install.
42
42
 
43
43
  Rule of thumb: `phewsh serve` = dispatch tasks *to* your agents.
44
44
  `phewsh mcp setup` = your agents pull tasks *from* PHEWSH mid-session.
@@ -82,7 +82,7 @@ ${isRefine ? '\nThis is a refinement of existing intent. The previous goal was:
82
82
  'content-type': 'application/json',
83
83
  },
84
84
  body: JSON.stringify({
85
- model: 'claude-sonnet-4-6',
85
+ model: require('../lib/providers').DEFAULT_ANTHROPIC_MODEL,
86
86
  max_tokens: 1024,
87
87
  system: systemPrompt,
88
88
  messages: [{ role: 'user', content: raw }],
package/commands/gate.js CHANGED
@@ -237,7 +237,7 @@ ${(plan || 'No plan yet').slice(0, 1500)}`;
237
237
  'content-type': 'application/json',
238
238
  },
239
239
  body: JSON.stringify({
240
- model: 'claude-sonnet-4-6',
240
+ model: require('../lib/providers').DEFAULT_ANTHROPIC_MODEL,
241
241
  max_tokens: 1024,
242
242
  messages: [{ role: 'user', content: prompt }],
243
243
  }),
@@ -261,7 +261,7 @@ ${(next || 'No next actions yet').slice(0, 2000)}`;
261
261
  'content-type': 'application/json',
262
262
  },
263
263
  body: JSON.stringify({
264
- model: 'claude-sonnet-4-6',
264
+ model: require('../lib/providers').DEFAULT_ANTHROPIC_MODEL,
265
265
  max_tokens: 4096,
266
266
  system: systemPrompt,
267
267
  messages: [{ role: 'user', content: userPrompt }],
package/commands/mcp.js CHANGED
@@ -1,15 +1,19 @@
1
1
  // phewsh mcp — Set up and sync the PHEWSH MCP coordination layer.
2
2
  //
3
+ // The MCP server ships inside this package (mcp/ — ESM subtree).
4
+ // No second install, no phewsh-mcp-server package.
5
+ //
3
6
  // Usage:
4
- // phewsh mcp setup Install MCP server deps + configure Claude Code
5
- // phewsh mcp sync — Sync local .intent/ + cloud projects → ~/.phewsh/projects.json
6
- // phewsh mcp status — Check what agents can see right now
7
- // phewsh mcp serve — Start the HTTP transport for the web bridge (:7483)
7
+ // phewsh mcp setup Configure Claude Code to use the bundled server
8
+ // phewsh mcp sync — Sync local .intent/ + cloud projects → ~/.phewsh/projects.json
9
+ // phewsh mcp status — Check what agents can see right now
10
+ // phewsh mcp serve — Start the HTTP transport for the web bridge (:7483)
11
+ // phewsh mcp serve --stdio — Run the stdio MCP server (what agent configs point at)
8
12
 
9
13
  const fs = require('fs');
10
14
  const path = require('path');
11
15
  const os = require('os');
12
- const { execSync, spawn } = require('child_process');
16
+ const { spawn } = require('child_process');
13
17
 
14
18
  const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
15
19
  const PROJECTS_FILE = path.join(PHEWSH_DIR, 'projects.json');
@@ -130,30 +134,16 @@ async function loadCloudProjects() {
130
134
  }
131
135
  }
132
136
 
133
- function findMcpServerPath() {
134
- // Check common locations
135
- const candidates = [
136
- path.join(__dirname, '..', '..', 'mcp', 'src', 'index.js'), // monorepo sibling
137
- path.join(process.cwd(), 'mcp', 'src', 'index.js'), // cwd
138
- ];
139
-
140
- // Also check if phewsh-mcp-server is globally installed
141
- try {
142
- const globalPath = execSync('npm root -g', { encoding: 'utf-8' }).trim();
143
- candidates.push(path.join(globalPath, 'phewsh-mcp-server', 'src', 'index.js'));
144
- } catch { /* not installed globally */ }
137
+ // The server is bundled with this package — no hunting.
138
+ const MCP_SERVER_PATH = path.join(__dirname, '..', 'mcp', 'index.js');
139
+ const MCP_HTTP_PATH = path.join(__dirname, '..', 'mcp', 'http-server.js');
145
140
 
146
- for (const p of candidates) {
147
- if (fs.existsSync(p)) return path.resolve(p);
148
- }
149
- return null;
141
+ function findMcpServerPath() {
142
+ return fs.existsSync(MCP_SERVER_PATH) ? MCP_SERVER_PATH : null;
150
143
  }
151
144
 
152
145
  function findHttpServerPath() {
153
- const stdioPath = findMcpServerPath();
154
- if (!stdioPath) return null;
155
- const httpPath = path.join(path.dirname(stdioPath), 'http-server.js');
156
- return fs.existsSync(httpPath) ? httpPath : null;
146
+ return fs.existsSync(MCP_HTTP_PATH) ? MCP_HTTP_PATH : null;
157
147
  }
158
148
 
159
149
  const DEFAULT_HTTP_PORT = 7483;
@@ -170,7 +160,25 @@ async function probeHttp(port = DEFAULT_HTTP_PORT) {
170
160
  }
171
161
  }
172
162
 
163
+ function serveStdio() {
164
+ // stdio MCP server: stdout IS the protocol — print nothing here.
165
+ ensureDirs();
166
+ const child = spawn(process.execPath, [MCP_SERVER_PATH], {
167
+ stdio: 'inherit',
168
+ env: { ...process.env },
169
+ });
170
+ child.on('exit', (code) => process.exit(code ?? 0));
171
+ const shutdown = () => { if (!child.killed) child.kill('SIGTERM'); };
172
+ process.on('SIGINT', shutdown);
173
+ process.on('SIGTERM', shutdown);
174
+ }
175
+
173
176
  async function serve() {
177
+ if (process.argv.includes('--stdio')) {
178
+ serveStdio();
179
+ return;
180
+ }
181
+
174
182
  console.log('');
175
183
  console.log(` ${b(w('PHEWSH MCP HTTP transport'))}`);
176
184
  console.log('');
@@ -179,8 +187,8 @@ async function serve() {
179
187
 
180
188
  const httpPath = findHttpServerPath();
181
189
  if (!httpPath) {
182
- console.log(` ${yellow('HTTP transport not found.')} Expected at mcp/src/http-server.js`);
183
- console.log(` ${g('Make sure you have the latest phewsh monorepo or reinstall the MCP package.')}`);
190
+ console.log(` ${yellow('HTTP transport not found.')} Expected bundled at ${g(MCP_HTTP_PATH)}`);
191
+ console.log(` ${g('Reinstall: npm i -g phewsh')}`);
184
192
  return;
185
193
  }
186
194
 
@@ -222,42 +230,24 @@ async function setup() {
222
230
 
223
231
  ensureDirs();
224
232
 
225
- // 1. Find or install MCP server
226
- let serverPath = findMcpServerPath();
233
+ // 1. Confirm the bundled server is present
234
+ const serverPath = findMcpServerPath();
227
235
 
228
236
  if (!serverPath) {
229
- console.log(` ${yellow('MCP server not found locally.')}`);
230
- console.log(` ${g('Install it:')}`);
231
- console.log(` npm install -g phewsh-mcp-server`);
232
- console.log('');
233
- console.log(` ${g('Or if you have the phewsh repo:')}`);
234
- console.log(` cd mcp && npm install`);
237
+ console.log(` ${yellow('Bundled MCP server missing.')} Reinstall: ${w('npm i -g phewsh')}`);
235
238
  console.log('');
236
239
  return;
237
240
  }
238
241
 
239
- console.log(` ${green('Found MCP server:')} ${g(serverPath)}`);
240
-
241
- // 2. Check if deps are installed
242
- const mcpDir = path.dirname(path.dirname(serverPath));
243
- const nodeModules = path.join(mcpDir, 'node_modules');
244
- if (!fs.existsSync(nodeModules)) {
245
- console.log(` ${yellow('Installing dependencies...')}`);
246
- try {
247
- execSync('npm install', { cwd: mcpDir, stdio: 'pipe' });
248
- console.log(` ${green('Dependencies installed.')}`);
249
- } catch (err) {
250
- console.log(` ${yellow('Failed to install deps.')} Run manually: cd ${mcpDir} && npm install`);
251
- }
252
- }
242
+ console.log(` ${green('MCP server:')} ${g('bundled with phewsh — no separate install')}`);
253
243
 
254
- // 3. Generate settings.json snippet
244
+ // 2. Generate settings.json snippet — points at phewsh itself, survives upgrades
255
245
  const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
256
246
  const snippet = {
257
247
  mcpServers: {
258
248
  phewsh: {
259
- command: 'node',
260
- args: [serverPath],
249
+ command: 'phewsh',
250
+ args: ['mcp', 'serve', '--stdio'],
261
251
  },
262
252
  },
263
253
  };
@@ -268,12 +258,16 @@ async function setup() {
268
258
  console.log(g(' ' + JSON.stringify(snippet, null, 2).split('\n').join('\n ')));
269
259
  console.log('');
270
260
 
271
- // Check if already configured
261
+ // Check if already configured — and flag stale configs from the two-package era
272
262
  if (fs.existsSync(claudeSettingsPath)) {
273
263
  try {
274
264
  const existing = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
275
- if (existing.mcpServers?.phewsh) {
265
+ const current = existing.mcpServers?.phewsh;
266
+ if (current?.command === 'phewsh') {
276
267
  console.log(` ${green('Already configured in Claude Code settings.')}`);
268
+ } else if (current) {
269
+ console.log(` ${yellow('Configured the old way')} (${g(current.command + ' ' + (current.args || []).join(' '))}).`);
270
+ console.log(` ${g('Update it to the snippet above — the server now ships inside phewsh.')}`);
277
271
  } else {
278
272
  console.log(` ${yellow('Not yet configured.')} Add the snippet above to your settings.`);
279
273
  }
@@ -442,10 +436,11 @@ async function main() {
442
436
  break;
443
437
  default:
444
438
  console.log(`\n ${b('phewsh mcp')} — Connect AI agents to your project intelligence\n`);
445
- console.log(` ${w('setup')} Install and configure the MCP server`);
446
- console.log(` ${w('sync')} Sync projects → ~/.phewsh/projects.json`);
447
- console.log(` ${w('status')} Check what agents can see right now`);
448
- console.log(` ${w('serve')} Run the HTTP bridge for the web app (:7483)`);
439
+ console.log(` ${w('setup')} Configure agents to use the bundled MCP server`);
440
+ console.log(` ${w('sync')} Sync projects → ~/.phewsh/projects.json`);
441
+ console.log(` ${w('status')} Check what agents can see right now`);
442
+ console.log(` ${w('serve')} Run the HTTP bridge for the web app (:7483)`);
443
+ console.log(` ${w('serve --stdio')} Run the stdio MCP server (for agent configs)`);
449
444
  console.log('');
450
445
  }
451
446
  }
@@ -95,14 +95,23 @@ function formatAgo(ms) {
95
95
  return `${days}d ago`;
96
96
  }
97
97
 
98
+ // Shortcuts, not a gate. Any string the user types that doesn't match an
99
+ // alias is passed through verbatim — the provider/harness validates it.
100
+ // PHEWSH must never block a model it hasn't heard of (it WILL go stale
101
+ // faster than the providers ship).
98
102
  const MODELS = {
103
+ 'claude-fable': { id: 'claude-fable-5', name: 'Claude Fable 5', provider: 'anthropic' },
104
+ 'claude-opus': { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', provider: 'anthropic' },
99
105
  'claude-sonnet': { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic' },
100
- 'claude-opus': { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'anthropic' },
101
106
  'claude-haiku': { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'anthropic' },
102
107
  };
103
108
 
104
109
  const DEFAULT_MODEL = 'claude-sonnet';
105
110
 
111
+ // currentModel may be an alias key OR a raw model id the user passed through.
112
+ function modelId(m) { return MODELS[m]?.id || m; }
113
+ function modelName(m) { return MODELS[m]?.name || m; }
114
+
106
115
  // ── Routing: where plain typed input goes ─────────────────────────────────
107
116
  // A route is either an installed harness (your existing subscription — no
108
117
  // API key needed in phewsh) or the direct API (your key). Precedence:
@@ -264,6 +273,7 @@ async function main() {
264
273
  try { recordProject(process.cwd()); } catch { /* index is best-effort */ }
265
274
  }
266
275
  let currentModel = DEFAULT_MODEL;
276
+ let harnessModel = null; // pass-through preference; the harness validates it
267
277
  let totalPromptTokens = 0;
268
278
  let totalCompletionTokens = 0;
269
279
 
@@ -456,7 +466,7 @@ async function main() {
456
466
  });
457
467
  decisionsThisSession++;
458
468
  try {
459
- const output = await runViaHarness(harnessId, fullSystem, buildHarnessPrompt(messages, input));
469
+ const output = await runViaHarness(harnessId, fullSystem, buildHarnessPrompt(messages, input), { model: harnessModel });
460
470
  messages.push({ role: 'user', content: input });
461
471
  messages.push({ role: 'assistant', content: (output || '').trim() });
462
472
  recordSessionEvent(harnessId, projectName, 'task_complete', {
@@ -484,18 +494,18 @@ async function main() {
484
494
  messages.push({ role: 'user', content: input });
485
495
  console.log('');
486
496
  try {
487
- const result = await streamChat(config.apiKey, messages, fullSystem, MODELS[currentModel].id);
497
+ const result = await streamChat(config.apiKey, messages, fullSystem, modelId(currentModel));
488
498
  messages.push({ role: 'assistant', content: result.content });
489
499
  if (result.promptTokens) totalPromptTokens += result.promptTokens;
490
500
  if (result.completionTokens) totalCompletionTokens += result.completionTokens;
491
501
  if (result.promptTokens || result.completionTokens) {
492
- console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name} · outcome? 1-4 or keep typing`));
502
+ console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${modelName(currentModel)} · outcome? 1-4 or keep typing`));
493
503
  }
494
504
  awaitingOutcome = decisionId;
495
505
  trackSap({
496
506
  userId: config.supabaseUserId,
497
507
  source: 'cli',
498
- model: MODELS[currentModel].id,
508
+ model: modelId(currentModel),
499
509
  promptTokens: result.promptTokens,
500
510
  completionTokens: result.completionTokens,
501
511
  accessToken: config.supabaseAccessToken,
@@ -720,6 +730,8 @@ async function main() {
720
730
  console.log('');
721
731
  console.log(` ${cream('route — where your typing goes')}`);
722
732
  console.log(` ${teal('/use')} ${slate('<route>')} ${sage('Switch: claude-code, codex, gemini, cursor, opencode, api')}`);
733
+ console.log(` ${teal('@name')} ${slate('<msg>')} ${sage('One message to one harness — @codex review this — context stays shared')}`);
734
+ console.log(` ${teal('/council')} ${slate('<q>')} ${sage('Ask ALL installed harnesses in parallel; keep the best answer')}`);
723
735
  console.log(` ${teal('/harnesses')} ${sage('Agent CLIs detected on this machine')}`);
724
736
  console.log(` ${teal('/provider')} ${sage('Current route + what\'s available')}`);
725
737
  console.log(` ${teal('/fallback')} ${sage('What happens at a usage wall: ask or auto-switch')}`);
@@ -735,7 +747,7 @@ async function main() {
735
747
  console.log(` ${cream('configure')}`);
736
748
  console.log(` ${teal('/key')} ${sage('Set API key (optional — harnesses need none)')}`);
737
749
  console.log(` ${teal('/login')} ${sage('Identity + cloud sync')}`);
738
- console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch API model (sonnet, opus, haiku)')}`);
750
+ console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model passed through, the provider validates')}`);
739
751
  console.log(` ${teal('/update')} ${sage('Update phewsh')}`);
740
752
  console.log(` ${teal('/tour')} ${sage('Quick walkthrough')}`);
741
753
  console.log('');
@@ -1079,19 +1091,51 @@ async function main() {
1079
1091
  const active = key === currentModel ? ` ${teal('●')}` : '';
1080
1092
  console.log(` ${cream(key.padEnd(16))} ${sage(model.name)}${active}`);
1081
1093
  }
1082
- console.log(`\n ${sage('Switch with:')} ${cream('/model <name>')}\n`);
1094
+ if (!MODELS[currentModel] && route?.type !== 'harness') {
1095
+ console.log(` ${cream(String(currentModel).padEnd(16))} ${sage('(pass-through)')} ${teal('●')}`);
1096
+ }
1097
+ console.log(`\n ${sage('Switch with:')} ${cream('/model <name>')} ${slate('— aliases above, or any model id (passed through)')}\n`);
1083
1098
  rl.prompt();
1084
1099
  return;
1085
1100
  }
1086
1101
 
1087
1102
  if (cmd === 'model') {
1103
+ // Harness route: the harness owns its model list — we pass the
1104
+ // preference through and let IT validate. No stale gate here.
1105
+ if (route?.type === 'harness') {
1106
+ const h = HARNESSES[route.id];
1107
+ if (!cmdArg) {
1108
+ console.log(` ${sage('Route:')} ${cream(h.label)} ${sage('— model:')} ${cream(harnessModel || h.label + ' default')}`);
1109
+ console.log(` ${sage('Usage:')} ${cream('/model <anything ' + h.label + ' accepts>')}${h.models ? '' : sage(' — not supported for this harness; set it in ' + h.label + ' itself')}`);
1110
+ console.log(` ${slate('/model default to clear')}`);
1111
+ rl.prompt();
1112
+ return;
1113
+ }
1114
+ if (!h.models) {
1115
+ console.log(` ${sage(h.label + ' doesn\'t take a model flag from phewsh — set the model in ' + h.label + ' itself.')}`);
1116
+ rl.prompt();
1117
+ return;
1118
+ }
1119
+ if (cmdArg.toLowerCase() === 'default') {
1120
+ harnessModel = null;
1121
+ console.log(` ${teal('●')} ${sage('Cleared — ' + h.label + ' uses its own default.')}`);
1122
+ } else {
1123
+ harnessModel = cmdArg.trim().replace(/\s+/g, '-');
1124
+ console.log(` ${teal('●')} ${sage('Model preference:')} ${cream(harnessModel)} ${slate('— ' + h.label + ' validates it on your next message')}`);
1125
+ }
1126
+ rl.prompt();
1127
+ return;
1128
+ }
1129
+
1130
+ // API route: aliases are shortcuts; anything else passes through
1131
+ // verbatim and the provider validates it.
1088
1132
  if (!cmdArg) {
1089
- console.log(` ${sage('Current:')} ${cream(MODELS[currentModel].name)}`);
1090
- console.log(` ${sage('Usage:')} ${cream('/model <sonnet|opus|haiku>')}`);
1133
+ console.log(` ${sage('Current:')} ${cream(modelName(currentModel))}`);
1134
+ console.log(` ${sage('Usage:')} ${cream('/model <' + Object.keys(MODELS).map(k => k.replace('claude-', '')).join('|') + '|any model id>')}`);
1091
1135
  rl.prompt();
1092
1136
  return;
1093
1137
  }
1094
- const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '');
1138
+ const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '').trim();
1095
1139
  const match = Object.keys(MODELS).find(k =>
1096
1140
  k.includes(query) || MODELS[k].name.toLowerCase().includes(query)
1097
1141
  );
@@ -1099,8 +1143,75 @@ async function main() {
1099
1143
  currentModel = match;
1100
1144
  console.log(` ${teal('●')} ${sage('Switched to')} ${cream(MODELS[match].name)}`);
1101
1145
  } else {
1102
- console.log(` ${sage('Unknown model. Available:')} ${cream(Object.keys(MODELS).join(', '))}`);
1146
+ currentModel = cmdArg.trim();
1147
+ console.log(` ${teal('●')} ${sage('Switched to')} ${cream(currentModel)} ${slate('— passed through as-is; the provider validates it')}`);
1148
+ }
1149
+ rl.prompt();
1150
+ return;
1151
+ }
1152
+
1153
+ if (cmd === 'council' || cmd === 'all') {
1154
+ // One prompt, every installed harness, in parallel. Different
1155
+ // models disagreeing is the signal — and which answer you KEEP
1156
+ // is the outcome data.
1157
+ const members = harnesses.filter(h => h.installed && h.headless);
1158
+ if (!cmdArg) {
1159
+ console.log(` ${sage('Usage:')} ${cream('/council <question>')} ${sage('— asks all ' + members.length + ' installed harnesses in parallel')}`);
1160
+ console.log(` ${slate(members.map(m => m.label).join(' · '))}`);
1161
+ rl.prompt();
1162
+ return;
1163
+ }
1164
+ if (members.length < 2) {
1165
+ console.log(` ${sage('Council needs at least 2 installed harnesses — you have ' + members.length + '.')}`);
1166
+ rl.prompt();
1167
+ return;
1168
+ }
1169
+ const councilHint = sessionMode
1170
+ ? Object.values(INTENT_MODES).find(m => m.id === sessionMode)?.hint
1171
+ : null;
1172
+ const councilSystem = councilHint ? `${systemPrompt}\n\n${councilHint}` : systemPrompt;
1173
+ const decisionId = recordDecision({
1174
+ project: projectName, route: 'council:' + members.map(m => m.id).join('+'),
1175
+ mode: sessionMode, summary: cmdArg,
1176
+ });
1177
+ decisionsThisSession++;
1178
+ console.log('');
1179
+ console.log(` ${b(cream('Council'))} ${sage('— asking')} ${cream(String(members.length))} ${sage('harnesses in parallel. Where they disagree is the insight.')}`);
1180
+ console.log(` ${slate(members.map(m => m.label).join(' · '))}`);
1181
+
1182
+ const prompt = buildHarnessPrompt(messages, cmdArg);
1183
+ const settled = await Promise.allSettled(members.map(m =>
1184
+ runViaHarness(m.id, councilSystem, prompt, { quiet: true })
1185
+ ));
1186
+
1187
+ const answers = [];
1188
+ settled.forEach((r, i) => {
1189
+ const m = members[i];
1190
+ console.log('');
1191
+ ui.divider('line');
1192
+ if (r.status === 'fulfilled') {
1193
+ const text = (r.value || '').trim();
1194
+ console.log(` ${b(cream(m.label))} ${slate('(' + m.role + ')')}`);
1195
+ console.log('');
1196
+ console.log(text.split('\n').map(l => ' ' + l).join('\n'));
1197
+ answers.push(`### ${m.label}\n${text}`);
1198
+ } else {
1199
+ console.log(` ${b(cream(m.label))} ${sage('failed')} ${slate('— ' + r.reason.message.split('\n')[0])}`);
1200
+ }
1201
+ });
1202
+ console.log('');
1203
+ ui.divider('line');
1204
+
1205
+ if (answers.length > 0) {
1206
+ messages.push({ role: 'user', content: cmdArg });
1207
+ messages.push({ role: 'assistant', content: `[council of ${answers.length}]\n\n${answers.join('\n\n')}` });
1208
+ awaitingOutcome = decisionId;
1209
+ console.log(slate(` council of ${answers.length} · outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing`));
1210
+ } else {
1211
+ try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
1212
+ console.log(` ${sage('Every council member failed — check')} ${cream('/provider')}`);
1103
1213
  }
1214
+ console.log('');
1104
1215
  rl.prompt();
1105
1216
  return;
1106
1217
  }
@@ -1120,7 +1231,8 @@ async function main() {
1120
1231
  }
1121
1232
  rows.push(['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '... (' + (config.provider || 'anthropic') + ')' : 'not set — optional', config?.apiKey ? 'green' : 'yellow']);
1122
1233
  rows.push(['Fallback', (config?.fallback === 'auto' ? 'auto-switch on failure' : 'ask before switching') + ' — /fallback to change', 'peach']);
1123
- if (route?.type === 'api') rows.push(['Model', MODELS[currentModel].name, 'cyan']);
1234
+ if (route?.type === 'api') rows.push(['Model', modelName(currentModel), 'cyan']);
1235
+ if (route?.type === 'harness' && harnessModel) rows.push(['Model', `${harnessModel} — passed to ${HARNESSES[route.id].label}`, 'cyan']);
1124
1236
  ui.statusPanel('Provider', rows);
1125
1237
  console.log(` ${sage('One terminal. Every AI worker. Shared project memory.')}`);
1126
1238
  console.log(` ${slate('switch:')} ${cream('/use <' + useOptions().join('|') + '>')} ${slate('· interactive tools: /work <hermes|pi>')}`);
@@ -1348,7 +1460,7 @@ async function main() {
1348
1460
  config.apiKey,
1349
1461
  [{ role: 'user', content: cmdArg }],
1350
1462
  systemPrompt,
1351
- MODELS[currentModel].id
1463
+ modelId(currentModel)
1352
1464
  );
1353
1465
  if (result.promptTokens || result.completionTokens) {
1354
1466
  console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
@@ -1356,7 +1468,7 @@ async function main() {
1356
1468
  trackSap({
1357
1469
  userId: config.supabaseUserId,
1358
1470
  source: 'cli',
1359
- model: MODELS[currentModel].id,
1471
+ model: modelId(currentModel),
1360
1472
  promptTokens: result.promptTokens,
1361
1473
  completionTokens: result.completionTokens,
1362
1474
  accessToken: config.supabaseAccessToken,
@@ -1390,6 +1502,26 @@ async function main() {
1390
1502
  : null;
1391
1503
  const fullSystem = modeHint ? `${systemPrompt}\n\n${modeHint}` : systemPrompt;
1392
1504
 
1505
+ // @mention: route ONE message to a specific harness without switching.
1506
+ // The answer lands in the shared history, so the next turn — on any
1507
+ // route — knows what was said. "@codex review this" mid-claude-session.
1508
+ const mention = input.match(/^@([\w-]+)\s+([\s\S]+)/);
1509
+ if (mention) {
1510
+ const q = mention[1].toLowerCase();
1511
+ const target = harnesses.find(h => h.installed && h.headless &&
1512
+ (h.id === q || h.id.startsWith(q) || h.label.toLowerCase().replace(/\s+/g, '-').startsWith(q)));
1513
+ if (!target) {
1514
+ console.log(` ${sage('No installed harness matches')} ${cream('@' + q)} ${sage('—')} ${cream('/provider')} ${sage('shows who\'s here.')}`);
1515
+ rl.prompt();
1516
+ return;
1517
+ }
1518
+ const okMention = await runHarnessTurn(mention[2], target.id, fullSystem);
1519
+ if (!okMention) await offerFallbacks(mention[2], fullSystem, target.id);
1520
+ console.log('');
1521
+ rl.prompt();
1522
+ return;
1523
+ }
1524
+
1393
1525
  const ok = route.type === 'harness'
1394
1526
  ? await runHarnessTurn(input, route.id, fullSystem)
1395
1527
  : await runApiTurn(input, fullSystem);
package/lib/harnesses.js CHANGED
@@ -14,15 +14,23 @@ const { execSync, spawn } = require('child_process');
14
14
  // args: how to run a one-shot prompt headlessly. args: null = we only know
15
15
  // how to launch it interactively (detection + /work still fully supported —
16
16
  // never guess flags; a wrong invocation looks like phewsh being broken).
17
+ //
18
+ // args takes (prompt, model). model is passed straight through to the
19
+ // harness's own flag and validated BY the harness — phewsh keeps no model
20
+ // list of its own, so it can never go stale. Harnesses without a known
21
+ // model flag ignore the preference and use their own config.
17
22
  const HARNESSES = {
18
- 'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', args: (p) => ['-p', p, '--output-format', 'text'] },
19
- 'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', args: (p) => ['exec', p] },
20
- 'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', args: (p) => ['-p', p] },
21
- 'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', auth: 'Cursor account', args: (p) => ['-p', p, '--output-format', 'text'] },
23
+ 'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
24
+ 'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', ...(m ? ['-m', m] : []), p] },
25
+ 'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', models: true, args: (p, m) => ['-p', p, ...(m ? ['-m', m] : [])] },
26
+ 'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', auth: 'Cursor account', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
22
27
  'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', auth: 'OpenCode Zen / configured', args: (p) => ['run', p] },
28
+ 'grok': { bin: 'grok', label: 'Grok Build', role: "xAI's take", auth: 'SuperGrok / X Premium+', args: (p) => ['-p', p] },
29
+ 'kiro': { bin: 'kiro-cli', label: 'Kiro CLI', role: 'spec-driven dev', auth: 'Kiro / AWS account', args: (p) => ['chat', '--no-interactive', p] },
30
+ 'copilot': { bin: 'copilot', label: 'Copilot CLI', role: 'github-native', auth: 'GitHub Copilot plan', args: (p) => ['-p', p] },
23
31
  'hermes': { bin: 'hermes', label: 'Hermes', role: 'runs loops', auth: 'Nous account', args: null },
24
32
  'pi': { bin: 'pi', label: 'Pi', role: 'conversation', auth: 'Pi login', args: null },
25
- 'aider': { bin: 'aider', label: 'Aider', role: 'pair-codes', auth: 'configured keys', args: (p) => ['--message', p] },
33
+ 'aider': { bin: 'aider', label: 'Aider', role: 'pair-codes', auth: 'configured keys', models: true, args: (p, m) => ['--message', p, ...(m ? ['--model', m] : [])] },
26
34
  'goose': { bin: 'goose', label: 'Goose', role: 'automates tasks', auth: 'Block / configured', args: (p) => ['run', '-t', p] },
27
35
  'amp': { bin: 'amp', label: 'Amp', role: 'agentic coding', auth: 'Sourcegraph account', args: (p) => ['-x', p] },
28
36
  'droid': { bin: 'droid', label: 'Droid', role: 'agentic coding', auth: 'Factory account', args: (p) => ['exec', p] },
@@ -51,24 +59,27 @@ function listHarnesses() {
51
59
  * stderr is buffered and only surfaced on failure (codex/gemini chat on it).
52
60
  * Resolves with the full stdout text so callers can keep conversation history.
53
61
  */
54
- function runViaHarness(id, systemPrompt, userPrompt) {
62
+ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
55
63
  const h = HARNESSES[id];
56
64
  if (!h) return Promise.reject(new Error(`Unknown harness: ${id}`));
57
65
  if (!h.args) return Promise.reject(new Error(`${h.label} is interactive-only here — launch it with /work ${id}`));
58
66
  const prompt = systemPrompt ? `${systemPrompt}\n\n---\n\n${userPrompt}` : userPrompt;
67
+ const model = h.models ? opts.model : undefined;
59
68
 
60
69
  return new Promise((resolve, reject) => {
61
- const child = spawn(h.bin, h.args(prompt), { stdio: ['pipe', 'pipe', 'pipe'] });
70
+ const child = spawn(h.bin, h.args(prompt, model), { stdio: ['pipe', 'pipe', 'pipe'] });
62
71
  // Some harnesses (codex exec, gemini) wait for stdin EOF before running.
63
72
  child.stdin.end();
64
73
 
65
74
  let stdout = '';
66
75
  let stderr = '';
67
- process.stdout.write('\n');
68
- child.stdout.on('data', (d) => { process.stdout.write(d); stdout += d.toString(); });
76
+ // quiet: collect without streaming — parallel runs (council) would
77
+ // interleave live output into soup.
78
+ if (!opts.quiet) process.stdout.write('\n');
79
+ child.stdout.on('data', (d) => { if (!opts.quiet) process.stdout.write(d); stdout += d.toString(); });
69
80
  child.stderr.on('data', (d) => { stderr += d.toString(); });
70
81
  child.on('close', (code) => {
71
- process.stdout.write('\n');
82
+ if (!opts.quiet) process.stdout.write('\n');
72
83
  if (code === 0) resolve(stdout);
73
84
  else reject(new Error(`${h.label} exited ${code}${stderr ? `\n ${stderr.trim().split('\n').slice(-3).join('\n ')}` : ''}`));
74
85
  });
package/lib/providers.js CHANGED
@@ -253,8 +253,12 @@ function streamParser(provider) {
253
253
  return provider.format === 'anthropic' ? streamAnthropicResponse : streamOpenAIResponse;
254
254
  }
255
255
 
256
+ // One place for the Anthropic default — commands must not pin their own.
257
+ const DEFAULT_ANTHROPIC_MODEL = PROVIDERS.anthropic.defaultModel;
258
+
256
259
  module.exports = {
257
260
  PROVIDERS,
261
+ DEFAULT_ANTHROPIC_MODEL,
258
262
  getProvider,
259
263
  listProviders,
260
264
  detectProvider,