phewsh 0.15.30 → 0.15.32

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/bin/phewsh.js CHANGED
@@ -189,6 +189,12 @@ if (!command) {
189
189
  exitAfterUpdate(0);
190
190
  } else if (COMMANDS[command]) {
191
191
  COMMANDS[command]();
192
+ } else if (require('../lib/harnesses').HARNESSES[command]) {
193
+ // `phewsh <harness>` — a doorway shortcut. Launch the session and have it
194
+ // auto-run /work for that harness (preflight → brief → native handoff →
195
+ // postflight). This is what the phewsh.com doorways copy as a single command.
196
+ process.env.PHEWSH_AUTOWORK = command;
197
+ maybeFirstRunIntro().then(() => COMMANDS.session());
192
198
  } else {
193
199
  console.error(`\n Unknown command: ${command}\n Run 'phewsh help' for available commands.\n`);
194
200
  process.exit(1);
@@ -17,13 +17,14 @@ const intentDir = () => path.join(process.cwd(), '.intent');
17
17
  const { select, refreshSession: refreshSess } = require('../lib/supabase');
18
18
  const { readPPS } = require('../lib/pps');
19
19
  const { push, pull, ensureValidToken } = require('./sync');
20
- const { HARNESSES, interactiveLaunchArgs, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
20
+ const { HARNESSES, INSTALL, interactiveLaunchArgs, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
21
21
  const { recordDecision, labelOutcome, pendingDecisions, recentDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
22
22
  const { suggest, suggestAll } = require('../lib/suggest');
23
23
  const continuity = require('../lib/continuity');
24
24
  const selfheal = require('../lib/selfheal');
25
25
  const learning = require('../lib/learning');
26
26
  const recall = require('../lib/recall');
27
+ const { routeCoach } = require('../lib/route-coach');
27
28
  const { closest } = require('../lib/closest');
28
29
  const cmdHistory = require('../lib/history');
29
30
  const { recordSessionEvent } = require('../lib/receipts-data');
@@ -55,6 +56,23 @@ const { recordProject, listProjects, scanForProjects, fmtAgo } = require('../lib
55
56
  const { b, d, w, g, green, cyan, yellow,
56
57
  teal, peach, sage, slate, cream, ember } = ui;
57
58
 
59
+ // Best-effort copy to the system clipboard so the brief survives the native
60
+ // tool taking over the terminal (and any trust/permission gate). Returns true
61
+ // if a clipboard tool accepted it. Never throws — it's a convenience.
62
+ function copyToClipboard(text) {
63
+ const { spawnSync } = require('child_process');
64
+ const tries = process.platform === 'darwin' ? [['pbcopy', []]]
65
+ : process.platform === 'win32' ? [['clip', []]]
66
+ : [['wl-copy', []], ['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']]];
67
+ for (const [bin, args] of tries) {
68
+ try {
69
+ const r = spawnSync(bin, args, { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
70
+ if (r.status === 0) return true;
71
+ } catch (_) { /* try next */ }
72
+ }
73
+ return false;
74
+ }
75
+
58
76
  // Sync awareness: compare local .intent/ timestamps with cloud updated_at
59
77
  async function checkSyncStatus(config) {
60
78
  if (!config?.supabaseUserId || !config?.supabaseAccessToken) return null;
@@ -180,6 +198,31 @@ const INTENT_MODES = {
180
198
  4: { id: 'review', label: 'Review', hint: 'The user wants critical review. Find what is wrong or risky before praising anything. Be specific about severity.' },
181
199
  };
182
200
 
201
+ const BARE_COMMANDS = {
202
+ help: '/help',
203
+ h: '/help',
204
+ next: '/next',
205
+ guide: '/next',
206
+ recommend: '/next',
207
+ tools: '/harnesses',
208
+ tool: '/harnesses',
209
+ agents: '/harnesses',
210
+ harnesses: '/harnesses',
211
+ routes: '/provider',
212
+ route: '/provider',
213
+ provider: '/provider',
214
+ status: '/status',
215
+ truth: '/truth',
216
+ brief: '/brief',
217
+ thread: '/thread',
218
+ outcomes: '/outcomes',
219
+ clarify: '/clarify',
220
+ setup: '/setup',
221
+ work: '/work',
222
+ quit: '/quit',
223
+ exit: '/quit',
224
+ };
225
+
183
226
  function loadConfig() {
184
227
  return configFile.loadConfig(CONFIG_PATH);
185
228
  }
@@ -348,10 +391,34 @@ async function main() {
348
391
  let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
349
392
  let pendingDidYouMean = null; // a suggested command; bare Enter accepts + runs it
350
393
  let decisionsThisSession = 0;
394
+ let lastRouteCoachId = null; // avoid repeating the same best-door hint
351
395
 
352
396
  // ── The Exhale: animated brand reveal ──────────────────
353
397
  await ui.brandReveal();
354
398
 
399
+ // Self-aware, plain-language summary — the value prop made real with THIS
400
+ // machine's state. Non-technical readers get one warm sentence; the rows
401
+ // below give engineers the exact facts. Fail-soft: a missing piece just
402
+ // shortens the line, it never blocks the door.
403
+ (function narrativeSummary() {
404
+ try {
405
+ const toolCount = harnesses.filter(h => h.installed).length;
406
+ const hasProject = intentFiles.length > 0;
407
+ const dot = teal('●');
408
+ const tools = `${toolCount} AI tool${toolCount !== 1 ? 's' : ''}`;
409
+ if (hasProject && toolCount > 0) {
410
+ console.log(` ${dot} ${sage(`${tools} here, all sharing one memory of`)} ${cream(projectName)}${sage(' — so you never start this project over.')}`);
411
+ } else if (hasProject) {
412
+ console.log(` ${dot} ${sage('phewsh holds one verified memory of')} ${cream(projectName)} ${sage('— ready for whatever AI tool you bring.')}`);
413
+ } else if (toolCount > 0) {
414
+ console.log(` ${dot} ${sage(`${tools} here.`)} ${sage('Run')} ${cream('/init')} ${sage('and they all work from one shared project memory.')}`);
415
+ } else {
416
+ console.log(` ${dot} ${sage('Install Claude Code, Codex, or Gemini — phewsh keeps them all on the same page.')}`);
417
+ }
418
+ console.log('');
419
+ } catch { /* the summary is a flourish, never a blocker */ }
420
+ })();
421
+
355
422
  if (config?.apiKey && !config.apiKey.startsWith('sk-')) {
356
423
  // Persist the cleanup so this never nags again — an unusable key
357
424
  // (not sk-…) helps nobody, and harness routes need no key at all.
@@ -563,13 +630,30 @@ async function main() {
563
630
  }
564
631
 
565
632
  // One subtle line under the menu — the single highest-leverage nudge, if any.
566
- function showInlineTip() {
633
+ function showInlineTip(filter = null) {
567
634
  let tip = null;
568
635
  try { tip = suggest(buildSuggestState()); } catch { /* never block the prompt on guidance */ }
569
636
  if (!tip) return;
637
+ if (filter && !filter(tip)) return;
570
638
  console.log(` ${teal('⤷')} ${sage(tip.message)} ${cream(tip.command.trim())} ${slate('· /next for options')}`);
571
639
  }
572
640
 
641
+ // The front door should steer, not trap. For a plain-language ask, point to
642
+ // the better native door when the pattern is obvious, then still run the turn.
643
+ function showRouteCoach(input) {
644
+ try {
645
+ const advice = routeCoach(input, {
646
+ route,
647
+ harnesses,
648
+ hasIntentDir: intentFiles.length > 0,
649
+ turnsThisSession: Math.floor(messages.length / 2),
650
+ });
651
+ if (!advice || advice.id === lastRouteCoachId) return;
652
+ lastRouteCoachId = advice.id;
653
+ console.log(` ${peach('↪')} ${sage(advice.message)} ${cream(advice.command)} ${slate('· still sending this turn')}`);
654
+ } catch { /* route coaching is advisory, never a blocker */ }
655
+ }
656
+
573
657
  // Open a known project from the bootstrap menu: chdir, reload memory,
574
658
  // back to the normal flow. The session is the cockpit; projects swap in.
575
659
  function openProjectAt(dir) {
@@ -669,6 +753,7 @@ async function main() {
669
753
  lastTurnFailure = null;
670
754
  awaitingOutcome = decisionId;
671
755
  console.log(slate(` via ${HARNESSES[harnessId].label} · how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing`));
756
+ showInlineTip((tip) => tip.id === 'capture-intent');
672
757
  return true;
673
758
  } catch (err) {
674
759
  turnInFlight = false;
@@ -719,6 +804,7 @@ async function main() {
719
804
  completionTokens: result.completionTokens,
720
805
  accessToken: config.supabaseAccessToken,
721
806
  });
807
+ showInlineTip((tip) => tip.id === 'capture-intent');
722
808
  return true;
723
809
  } catch (err) {
724
810
  turnInFlight = false;
@@ -1040,6 +1126,16 @@ async function main() {
1040
1126
  // Any typed input supersedes a pending "did you mean" offer.
1041
1127
  if (pendingDidYouMean) pendingDidYouMean = null;
1042
1128
 
1129
+ // Exact bare words are front-door controls too. Keep this exact-match only:
1130
+ // "help" opens help; "help me build X" still routes as natural language.
1131
+ if (!input.startsWith('/')) {
1132
+ const bare = input.trim().toLowerCase();
1133
+ if (BARE_COMMANDS[bare]) {
1134
+ await handleInput(BARE_COMMANDS[bare]);
1135
+ return;
1136
+ }
1137
+ }
1138
+
1043
1139
  // A bare number right after a route failure picks the fallback
1044
1140
  if (awaitingFallback) {
1045
1141
  const af = awaitingFallback;
@@ -1565,15 +1661,14 @@ async function main() {
1565
1661
  }
1566
1662
 
1567
1663
  if (cmd === 'clarify') {
1568
- if (!config?.apiKey) {
1569
- console.log(`\n ${ember('!')} ${sage('No API key. Run /key to set one.')}\n`);
1570
- rl.prompt();
1571
- return;
1572
- }
1573
1664
  try {
1574
- const { execSync } = require('child_process');
1575
- const args = cmdArg ? `--text "${cmdArg.replace(/"/g, '\\"')}"` : '';
1576
- execSync(`node ${path.join(__dirname, 'clarify.js')} ${args}`, { stdio: 'inherit' });
1665
+ const { spawnSync } = require('child_process');
1666
+ const args = ['clarify'];
1667
+ if (cmdArg) args.push('--text', cmdArg);
1668
+ const res = spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), ...args], { stdio: 'inherit' });
1669
+ if (res.status !== 0) {
1670
+ console.error(` ${ember('!')} ${sage('Clarify exited without writing context.')}`);
1671
+ }
1577
1672
  intentFiles = loadIntentContext();
1578
1673
  systemPrompt = buildSystemPrompt(intentFiles);
1579
1674
  if (intentFiles.length > 0) {
@@ -2092,7 +2187,7 @@ async function main() {
2092
2187
  }
2093
2188
  if (!h.installed) continue; // hide the long tail of uninstalled extras
2094
2189
  const via = h.headless ? '' : ' · /work only';
2095
- rows.push([h.label, `ready — ${h.role}${via}`, 'green']);
2190
+ rows.push([h.label, `ready — ${h.bestFor || h.role}${via}`, 'green']);
2096
2191
  }
2097
2192
  rows.push(['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '... (' + (config.provider || 'anthropic') + ')' : 'not set — optional', config?.apiKey ? 'green' : 'yellow']);
2098
2193
  rows.push(['Fallback', (config?.fallback === 'auto' ? 'auto-switch on failure' : 'ask before switching') + ' — /fallback to change', 'peach']);
@@ -2184,7 +2279,7 @@ async function main() {
2184
2279
  const mode = h.headless ? '' : slate(' · /work only');
2185
2280
  const badge = hStats ? learning.keptBadge(hStats, h.id) : '';
2186
2281
  const rec = badge ? ` ${slate('· ' + badge)}` : '';
2187
- console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.role || h.label).padEnd(20))} ${slate(h.label)}${mode}${rec}${active}`);
2282
+ console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.bestFor || h.role || h.label).padEnd(38))} ${slate(h.label)}${mode}${rec}${active}`);
2188
2283
  }
2189
2284
  ui.divider('line');
2190
2285
  const learned = hStats ? learning.learningLine(hStats) : null;
@@ -2289,7 +2384,16 @@ async function main() {
2289
2384
  return;
2290
2385
  }
2291
2386
  if (!harnesses.find(x => x.id === target)?.installed) {
2292
- console.log(` ${ember('!')} ${sage(h.label + ' is not installed on this machine.')}`);
2387
+ const how = INSTALL[target];
2388
+ console.log('');
2389
+ console.log(` ${ember('!')} ${sage(h.label + ' isn\'t installed on this machine yet.')}`);
2390
+ if (how) {
2391
+ const isCmd = !how.startsWith('see ');
2392
+ console.log(` ${sage('Install:')} ${isCmd ? cream(how) : slate(how)}`);
2393
+ }
2394
+ if (h.auth) console.log(` ${sage('Sign in:')} ${slate(h.auth + ' (the tool prompts you on first run)')}`);
2395
+ console.log(` ${sage('Then run:')} ${cream('phewsh ' + target)} ${slate('— phewsh briefs it and hands off')}`);
2396
+ console.log('');
2293
2397
  rl.prompt();
2294
2398
  return;
2295
2399
  }
@@ -2297,9 +2401,13 @@ async function main() {
2297
2401
  const preflightTruth = await auditTruth();
2298
2402
  const before = captureSnapshot(preflightTruth);
2299
2403
  const generatedBrief = await generateBrief({ report: preflightTruth });
2300
- const launchBrief = `${generatedBrief.content}\n\nThis is a PHEWSH transition briefing. Use it as project context, preserve native tool behavior, and verify claims against the repository before acting. When you finish, exit this tool — PHEWSH resumes, runs an automatic postflight comparing what changed against this brief, and offers reconciliation.`;
2404
+ const launchBrief = `${generatedBrief.content}\n\nYou are receiving a PHEWSH handoff brief — acknowledge in one short line that you've loaded it, then proceed. Use it as project context, preserve native tool behavior, and verify claims against the repository before acting. When you finish, exit this tool — PHEWSH resumes, runs an automatic postflight comparing what changed against this brief, and offers reconciliation.`;
2301
2405
  const savedBrief = persistBrief(generatedBrief.content, { project: projectName, route: target });
2302
2406
  const launch = interactiveLaunchArgs(target, launchBrief, { model: harnessModel });
2407
+ // Foolproof fallback: the brief on the clipboard survives the native
2408
+ // tool taking over the terminal and any trust/permission gate. If the
2409
+ // tool ignores or never received it, the human just pastes it in.
2410
+ const briefOnClipboard = copyToClipboard(launchBrief);
2303
2411
  const decisionId = recordDecision({
2304
2412
  project: projectName,
2305
2413
  route: target,
@@ -2313,8 +2421,13 @@ async function main() {
2313
2421
  console.log(` ${sage('Route:')} ${cream(h.label)}${harnessModel ? slate(' · model ' + harnessModel) : slate(' · native default model')}`);
2314
2422
  console.log(` ${sage('Git:')} ${cream(preflightTruth.git.shortHead || 'unknown')} ${slate('· ' + before.dirty.length + ' uncommitted path(s) before work')}`);
2315
2423
  console.log(` ${sage('Truth:')} ${preflightTruth.conflicts.length ? peach(preflightTruth.conflicts.length + ' conflict(s) carried explicitly') : green('no explicit conflicts')}`);
2316
- console.log(` ${sage('Brief:')} ${launch.briefingPassed ? green('passed directly to the native harness') : peach('not injectable for this harness; available via /brief')}`);
2317
- console.log(` ${sage('Record:')} ${savedBrief.written ? slate('exact briefing saved locally for this transition') : peach('briefing persistence unavailable; launch continues')}`);
2424
+ console.log(` ${sage('Brief:')} ${launch.briefingPassed ? green('auto-attached to ' + h.label) : peach('not auto-injectable for this tool')}${briefOnClipboard ? slate(' · copied to your clipboard') : ''}`);
2425
+ console.log(` ${sage('Record:')} ${savedBrief.written ? slate('exact briefing saved locally' + (savedBrief.file ? ' → ' + savedBrief.file : '')) : peach('briefing persistence unavailable; launch continues')}`);
2426
+ if (briefOnClipboard) {
2427
+ console.log(` ${slate('Foolproof:')} ${sage('if ' + h.label + ' shows a trust prompt or doesn\'t mention the brief, just paste it in')} ${slate('(⌘V / Ctrl+V)')}`);
2428
+ } else if (savedBrief.file) {
2429
+ console.log(` ${slate('Foolproof:')} ${sage('if ' + h.label + ' doesn\'t pick up the brief, paste from')} ${cream(savedBrief.file)}`);
2430
+ }
2318
2431
  console.log(` ${slate('After exit PHEWSH will compare Git, files, intent claims, generated drift, and contradictions.')}`);
2319
2432
  ui.divider('line');
2320
2433
  console.log(` ${teal('●')} ${sage('Handing the terminal to')} ${cream(h.label)} ${slate('— exit to return for postflight')}`);
@@ -2420,8 +2533,10 @@ async function main() {
2420
2533
  // Regular input → route it (harness = your subscription, api = your key)
2421
2534
  if (!route) {
2422
2535
  console.log('');
2423
- console.log(` ${sage('No route yet')} ${cream('/key')} ${sage('to set an API key, or install Claude Code / Codex')}`);
2424
- console.log(` ${slate('phewsh uses installed agent CLIs automatically, no key needed.')}`);
2536
+ console.log(` ${sage('That request is valid phewsh just needs an AI worker behind the door.')}`);
2537
+ console.log(` ${cream('phewsh setup')} ${sage('detects installed tools and picks a route')}`);
2538
+ console.log(` ${cream('/key')} ${sage('adds an API key, or install Claude Code / Codex / Gemini and rerun phewsh')}`);
2539
+ console.log(` ${slate('Once a route exists, plain typing works; no slash command needed.')}`);
2425
2540
  console.log('');
2426
2541
  rl.prompt();
2427
2542
  return;
@@ -2452,6 +2567,8 @@ async function main() {
2452
2567
  return;
2453
2568
  }
2454
2569
 
2570
+ showRouteCoach(input);
2571
+
2455
2572
  const ok = route.type === 'harness'
2456
2573
  ? await runHarnessTurn(input, route.id, fullSystem)
2457
2574
  : await runApiTurn(input, fullSystem);
@@ -2490,6 +2607,14 @@ async function main() {
2490
2607
  console.log(`\n ${sage('session ended')}\n`);
2491
2608
  process.exit(0);
2492
2609
  });
2610
+
2611
+ // Doorway shortcut: `phewsh <harness>` sets PHEWSH_AUTOWORK so we drop the
2612
+ // user straight into /work for that tool after the front door renders.
2613
+ const autoWork = process.env.PHEWSH_AUTOWORK;
2614
+ if (autoWork && HARNESSES[autoWork]) {
2615
+ delete process.env.PHEWSH_AUTOWORK;
2616
+ handleInput('/work ' + autoWork).catch(() => {});
2617
+ }
2493
2618
  }
2494
2619
 
2495
2620
  main().catch(err => {
package/commands/setup.js CHANGED
@@ -47,7 +47,8 @@ module.exports = async function setup() {
47
47
  for (const h of harnesses) {
48
48
  const status = h.installed ? green('✓ installed') : slate('✗ not installed');
49
49
  const mode = h.headless ? '' : slate(' · interactive — /work ' + h.id);
50
- console.log(` ${cream(h.label.padEnd(14))} ${status} ${slate('(' + h.auth + ')')}${mode}`);
50
+ const fit = h.bestFor ? slate(' · best for ' + h.bestFor) : '';
51
+ console.log(` ${cream(h.label.padEnd(14))} ${status} ${slate('(' + h.auth + ')')}${mode}${fit}`);
51
52
  }
52
53
  if (installed.length === 0) {
53
54
  console.log('');
@@ -95,7 +96,7 @@ module.exports = async function setup() {
95
96
  // ── 2. Pick the default route ─────────────────────────
96
97
  // Interactive-only harnesses (Hermes, Pi) can't take chat routing — they
97
98
  // stay reachable via /work in a session, so they're not default options.
98
- const options = chatCapable.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} — your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
99
+ const options = chatCapable.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} — ${h.bestFor || h.role}; your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
99
100
  options.push({ kind: 'api', id: 'api', label: 'Direct API — bring your own Anthropic/OpenRouter key' });
100
101
 
101
102
  console.log(` ${b(cream('Where should phewsh route your work by default?'))}`);
package/lib/harnesses.js CHANGED
@@ -20,20 +20,41 @@ const { execSync, spawn } = require('child_process');
20
20
  // list of its own, so it can never go stale. Harnesses without a known
21
21
  // model flag ignore the preference and use their own config.
22
22
  const HARNESSES = {
23
- 'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, modelHints: ['sonnet', 'opus', 'haiku'], streamFormat: 'claude-json', args: (p, m) => ['-p', p, '--output-format', 'stream-json', '--include-partial-messages', '--verbose', ...(m ? ['--model', m] : [])], interactiveArgs: (brief, m) => ['--append-system-prompt', brief, ...(m ? ['--model', m] : [])] },
24
- 'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', '--skip-git-repo-check', ...(m ? ['-m', m] : []), p], interactiveArgs: (brief, m) => [...(m ? ['-m', m] : []), brief] },
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] : [])], interactiveArgs: (brief, m) => ['--prompt-interactive', brief, ...(m ? ['--model', 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] : [])] },
27
- 'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', auth: 'OpenCode Zen / configured', args: (p) => ['run', p], interactiveArgs: (brief) => ['--prompt', brief] },
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] },
31
- 'hermes': { bin: 'hermes', label: 'Hermes', role: 'runs loops', auth: 'Nous account', args: null },
32
- 'pi': { bin: 'pi', label: 'Pi', role: 'conversation', auth: 'Pi login', args: null },
33
- 'aider': { bin: 'aider', label: 'Aider', role: 'pair-codes', auth: 'configured keys', models: true, args: (p, m) => ['--message', p, ...(m ? ['--model', m] : [])] },
34
- 'goose': { bin: 'goose', label: 'Goose', role: 'automates tasks', auth: 'Block / configured', args: (p) => ['run', '-t', p] },
35
- 'amp': { bin: 'amp', label: 'Amp', role: 'agentic coding', auth: 'Sourcegraph account', args: (p) => ['-x', p] },
36
- 'droid': { bin: 'droid', label: 'Droid', role: 'agentic coding', auth: 'Factory account', args: (p) => ['exec', p] },
23
+ 'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', bestFor: 'repo edits, tests, native coding loops', auth: 'Claude subscription / Console', models: true, modelHints: ['sonnet', 'opus', 'haiku'], streamFormat: 'claude-json', args: (p, m) => ['-p', p, '--output-format', 'stream-json', '--include-partial-messages', '--verbose', ...(m ? ['--model', m] : [])], interactiveArgs: (brief, m) => ['--append-system-prompt', brief, ...(m ? ['--model', m] : [])] },
24
+ 'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', bestFor: 'reviews, reasoning, second opinions', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', '--skip-git-repo-check', ...(m ? ['-m', m] : []), p], interactiveArgs: (brief, m) => [...(m ? ['-m', m] : []), brief] },
25
+ 'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", bestFor: 'broad scans, alternate framing', auth: 'Google login', models: true, args: (p, m) => ['-p', p, ...(m ? ['-m', m] : [])], interactiveArgs: (brief, m) => ['--prompt-interactive', brief, ...(m ? ['--model', m] : [])] },
26
+ 'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', bestFor: 'editor-side file edits', auth: 'Cursor account', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
27
+ 'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', bestFor: 'open-source agent work', auth: 'OpenCode Zen / configured', args: (p) => ['run', p], interactiveArgs: (brief) => ['--prompt', brief] },
28
+ 'grok': { bin: 'grok', label: 'Grok Build', role: "xAI's take", bestFor: 'outside take from xAI', auth: 'SuperGrok / X Premium+', args: (p) => ['-p', p] },
29
+ 'kiro': { bin: 'kiro-cli', label: 'Kiro CLI', role: 'spec-driven dev', bestFor: 'spec-driven development', auth: 'Kiro / AWS account', args: (p) => ['chat', '--no-interactive', p] },
30
+ 'copilot': { bin: 'copilot', label: 'Copilot CLI', role: 'github-native', bestFor: 'GitHub-native tasks', auth: 'GitHub Copilot plan', args: (p) => ['-p', p] },
31
+ 'hermes': { bin: 'hermes', label: 'Hermes', role: 'runs loops', bestFor: 'orchestration and continuity', auth: 'Nous account', args: null },
32
+ 'pi': { bin: 'pi', label: 'Pi', role: 'conversation', bestFor: 'conversation', auth: 'Pi login', args: null },
33
+ 'aider': { bin: 'aider', label: 'Aider', role: 'pair-codes', bestFor: 'pair-programming patches', auth: 'configured keys', models: true, args: (p, m) => ['--message', p, ...(m ? ['--model', m] : [])] },
34
+ 'goose': { bin: 'goose', label: 'Goose', role: 'automates tasks', bestFor: 'automation runs', auth: 'Block / configured', args: (p) => ['run', '-t', p] },
35
+ 'amp': { bin: 'amp', label: 'Amp', role: 'agentic coding', bestFor: 'agentic coding', auth: 'Sourcegraph account', args: (p) => ['-x', p] },
36
+ 'droid': { bin: 'droid', label: 'Droid', role: 'agentic coding', bestFor: 'agentic coding', auth: 'Factory account', args: (p) => ['exec', p] },
37
+ };
38
+
39
+ // Best-effort install hints, shown when a harness isn't on the machine yet.
40
+ // Commands where the install path is well-known and stable; a docs pointer
41
+ // otherwise. We never auto-run these — phewsh shows them; the human decides.
42
+ // (Install paths and auth change upstream; treat as a guidepost, not gospel.)
43
+ const INSTALL = {
44
+ 'claude-code': 'npm i -g @anthropic-ai/claude-code',
45
+ 'codex': 'npm i -g @openai/codex',
46
+ 'gemini': 'npm i -g @google/gemini-cli',
47
+ 'cursor': 'curl https://cursor.com/install -fsS | bash',
48
+ 'opencode': 'npm i -g opencode-ai',
49
+ 'grok': 'npm i -g @vibe-kit/grok-cli',
50
+ 'copilot': 'npm i -g @github/copilot',
51
+ 'kiro': 'see kiro.dev/downloads',
52
+ 'hermes': 'see nousresearch.com',
53
+ 'pi': 'see pi.ai',
54
+ 'aider': 'python -m pip install aider-chat',
55
+ 'goose': 'see block.github.io/goose',
56
+ 'amp': 'npm i -g @sourcegraph/amp',
57
+ 'droid': 'see factory.ai',
37
58
  };
38
59
 
39
60
  // In-flight harness children — so ESC in the session can cancel a turn.
@@ -199,6 +220,7 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
199
220
 
200
221
  module.exports = {
201
222
  HARNESSES,
223
+ INSTALL,
202
224
  isInstalled,
203
225
  detectInstalled,
204
226
  interactiveLaunchArgs,
package/lib/intro.js CHANGED
@@ -83,7 +83,7 @@ async function playIntro(opts = {}) {
83
83
  out(` ${slate('· none found yet — install Claude Code, Codex, or Gemini and phewsh picks it up.')}`);
84
84
  } else {
85
85
  for (const h of harnesses) {
86
- out(` ${green('✓')} ${cream(h.label.padEnd(14))} ${sage(h.role || '')}`);
86
+ out(` ${green('✓')} ${cream(h.label.padEnd(14))} ${sage(h.bestFor || h.role || '')}`);
87
87
  await pause(130);
88
88
  }
89
89
  await pause(160);
@@ -93,7 +93,11 @@ async function playIntro(opts = {}) {
93
93
  }
94
94
  out('');
95
95
  await pause(160);
96
- out(` ${sage('Next:')} ${cream('just type to start')} ${slate('·')} ${cream('phewsh setup')} ${slate('to pick a default route')}`);
96
+ if (harnesses.length > 0) {
97
+ out(` ${sage('Next:')} ${cream('just type to start')} ${slate('·')} ${cream('phewsh setup')} ${slate('to pick a default route')}`);
98
+ } else {
99
+ out(` ${sage('Next:')} ${cream('phewsh setup')} ${slate('after installing Claude Code, Codex, or Gemini')}`);
100
+ }
97
101
  out('');
98
102
 
99
103
  return { toolsFound: harnesses.length };
@@ -0,0 +1,91 @@
1
+ // Route coach — tiny deterministic hints for the front door.
2
+ //
3
+ // PHEWSH should not become a new agent. It should notice when the user's
4
+ // message is better served by a native harness, a second opinion, or a council
5
+ // and point to that door without blocking the current turn.
6
+
7
+ const CODE_RE = /\b(implement|edit|change|patch|fix|debug|bug|refactor|test|tests|failing|failure|build|ship|commit|diff|file|code|function|module|schema|migration|auth|deploy|npm|lint)\b/i;
8
+ const STRONG_CODE_RE = /\b(implement|edit|change|patch|fix|debug|refactor|failing|failure|commit|schema|migration|auth|deploy|lint)\b/i;
9
+ const REVIEW_RE = /\b(review|audit|critique|risk|risks|regression|security|edge case|edge cases|sanity check)\b/i;
10
+ const COMPARE_RE = /\b(compare|options|which|tradeoff|trade-off|pros and cons|brainstorm|strategy|what should i do|where should i start|best approach|decide)\b/i;
11
+ const INTENT_RE = /\b(i want to build|idea|project|startup|app|tool|product|trying to build|what i'm building|what i am building)\b/i;
12
+
13
+ function routeId(route) {
14
+ if (!route) return null;
15
+ if (typeof route === 'string') return route;
16
+ return route.id || route.type || null;
17
+ }
18
+
19
+ function installed(harnesses, id) {
20
+ return (harnesses || []).find((h) => h.id === id && h.installed);
21
+ }
22
+
23
+ function headless(harnesses) {
24
+ return (harnesses || []).filter((h) => h.installed && h.headless);
25
+ }
26
+
27
+ function firstInstalled(harnesses, ids) {
28
+ return ids.map((id) => installed(harnesses, id)).find(Boolean) || null;
29
+ }
30
+
31
+ function canInjectBrief(h) {
32
+ return !!h && typeof h.interactiveArgs === 'function';
33
+ }
34
+
35
+ function routeCoach(input, {
36
+ route = null,
37
+ harnesses = [],
38
+ hasIntentDir = false,
39
+ turnsThisSession = 0,
40
+ } = {}) {
41
+ const text = String(input || '').trim();
42
+ if (!text || text.startsWith('/') || text.startsWith('@')) return null;
43
+
44
+ const current = routeId(route);
45
+ const codeish = CODE_RE.test(text);
46
+ const strongCode = STRONG_CODE_RE.test(text);
47
+ const reviewish = REVIEW_RE.test(text);
48
+ const compareish = COMPARE_RE.test(text);
49
+
50
+ if (!hasIntentDir && turnsThisSession === 0 && INTENT_RE.test(text) && text.length >= 40) {
51
+ return {
52
+ id: 'clarify-first',
53
+ command: '/clarify',
54
+ message: 'This sounds like raw project intent; clarify turns it into memory every tool can share.',
55
+ };
56
+ }
57
+
58
+ if (codeish && (strongCode || text.length >= 80)) {
59
+ const target = firstInstalled(harnesses, ['claude-code', 'opencode', 'codex', 'gemini']);
60
+ if (canInjectBrief(target)) {
61
+ return {
62
+ id: `native-work:${target.id}`,
63
+ command: `/work ${target.id}`,
64
+ message: `${target.label} native mode is the better door for repo changes; phewsh will verify before and after.`,
65
+ };
66
+ }
67
+ }
68
+
69
+ if (reviewish && !strongCode) {
70
+ const codex = installed(harnesses, 'codex');
71
+ if (codex && current !== 'codex') {
72
+ return {
73
+ id: 'review-with-codex',
74
+ command: '@codex <same ask>',
75
+ message: 'For review, use a second model without switching the whole session.',
76
+ };
77
+ }
78
+ }
79
+
80
+ if (compareish && headless(harnesses).length >= 2) {
81
+ return {
82
+ id: 'ask-council',
83
+ command: '/council <same ask>',
84
+ message: 'This is a judgment call; ask every installed tool and keep the best answer.',
85
+ };
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ module.exports = { routeCoach };
package/lib/suggest.js CHANGED
@@ -46,11 +46,12 @@ function suggestAll(s = {}) {
46
46
 
47
47
  // 1. Working without captured intent — the biggest miss. Every AI tool here
48
48
  // could be reading the same goal; right now none are.
49
- if (intentFileCount === 0 && turnsThisSession >= 2) {
49
+ if (intentFileCount === 0 && turnsThisSession >= 1) {
50
+ const noun = turnsThisSession === 1 ? 'message' : 'messages';
50
51
  out.push({
51
52
  id: 'capture-intent',
52
53
  priority: 100,
53
- message: `${turnsThisSession} messages in, no captured intent — every AI here is guessing.`,
54
+ message: `${turnsThisSession} ${noun} in, no captured intent — every AI here is guessing.`,
54
55
  command: '/clarify',
55
56
  why: 'Turns what you just said into a spec every tool (this one, Claude Code, Codex) reads.',
56
57
  });
package/lib/ui.js CHANGED
@@ -98,7 +98,16 @@ async function brandReveal(fast = false) {
98
98
  console.log(` ${b(cream('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
99
99
  console.log(` ${b(cream('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
100
100
  await pause(160);
101
- console.log(` ${sage(".intent/ is your project's working memory.")}`);
101
+ // The value prop, plain language, off the bat — the approved north-star line.
102
+ // It exhales in (typewriter) when animated; lands instantly otherwise. Safe:
103
+ // the color wrapper is emitted up front, so only the words reveal.
104
+ const tagline = 'Stop re-explaining yourself to AI.';
105
+ if (fast || !process.stdout.isTTY) {
106
+ console.log(` ${sage(tagline)}`);
107
+ } else {
108
+ process.stdout.write(' ');
109
+ await typewrite(sage(tagline), 20);
110
+ }
102
111
  console.log('');
103
112
  }
104
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.30",
3
+ "version": "0.15.32",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"