phewsh 0.15.30 → 0.15.31

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);
@@ -24,6 +24,7 @@ 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');
@@ -180,6 +181,31 @@ const INTENT_MODES = {
180
181
  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
182
  };
182
183
 
184
+ const BARE_COMMANDS = {
185
+ help: '/help',
186
+ h: '/help',
187
+ next: '/next',
188
+ guide: '/next',
189
+ recommend: '/next',
190
+ tools: '/harnesses',
191
+ tool: '/harnesses',
192
+ agents: '/harnesses',
193
+ harnesses: '/harnesses',
194
+ routes: '/provider',
195
+ route: '/provider',
196
+ provider: '/provider',
197
+ status: '/status',
198
+ truth: '/truth',
199
+ brief: '/brief',
200
+ thread: '/thread',
201
+ outcomes: '/outcomes',
202
+ clarify: '/clarify',
203
+ setup: '/setup',
204
+ work: '/work',
205
+ quit: '/quit',
206
+ exit: '/quit',
207
+ };
208
+
183
209
  function loadConfig() {
184
210
  return configFile.loadConfig(CONFIG_PATH);
185
211
  }
@@ -348,10 +374,34 @@ async function main() {
348
374
  let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
349
375
  let pendingDidYouMean = null; // a suggested command; bare Enter accepts + runs it
350
376
  let decisionsThisSession = 0;
377
+ let lastRouteCoachId = null; // avoid repeating the same best-door hint
351
378
 
352
379
  // ── The Exhale: animated brand reveal ──────────────────
353
380
  await ui.brandReveal();
354
381
 
382
+ // Self-aware, plain-language summary — the value prop made real with THIS
383
+ // machine's state. Non-technical readers get one warm sentence; the rows
384
+ // below give engineers the exact facts. Fail-soft: a missing piece just
385
+ // shortens the line, it never blocks the door.
386
+ (function narrativeSummary() {
387
+ try {
388
+ const toolCount = harnesses.filter(h => h.installed).length;
389
+ const hasProject = intentFiles.length > 0;
390
+ const dot = teal('●');
391
+ const tools = `${toolCount} AI tool${toolCount !== 1 ? 's' : ''}`;
392
+ if (hasProject && toolCount > 0) {
393
+ console.log(` ${dot} ${sage(`${tools} here, all sharing one memory of`)} ${cream(projectName)}${sage(' — so you never start this project over.')}`);
394
+ } else if (hasProject) {
395
+ console.log(` ${dot} ${sage('phewsh holds one verified memory of')} ${cream(projectName)} ${sage('— ready for whatever AI tool you bring.')}`);
396
+ } else if (toolCount > 0) {
397
+ console.log(` ${dot} ${sage(`${tools} here.`)} ${sage('Run')} ${cream('/init')} ${sage('and they all work from one shared project memory.')}`);
398
+ } else {
399
+ console.log(` ${dot} ${sage('Install Claude Code, Codex, or Gemini — phewsh keeps them all on the same page.')}`);
400
+ }
401
+ console.log('');
402
+ } catch { /* the summary is a flourish, never a blocker */ }
403
+ })();
404
+
355
405
  if (config?.apiKey && !config.apiKey.startsWith('sk-')) {
356
406
  // Persist the cleanup so this never nags again — an unusable key
357
407
  // (not sk-…) helps nobody, and harness routes need no key at all.
@@ -563,13 +613,30 @@ async function main() {
563
613
  }
564
614
 
565
615
  // One subtle line under the menu — the single highest-leverage nudge, if any.
566
- function showInlineTip() {
616
+ function showInlineTip(filter = null) {
567
617
  let tip = null;
568
618
  try { tip = suggest(buildSuggestState()); } catch { /* never block the prompt on guidance */ }
569
619
  if (!tip) return;
620
+ if (filter && !filter(tip)) return;
570
621
  console.log(` ${teal('⤷')} ${sage(tip.message)} ${cream(tip.command.trim())} ${slate('· /next for options')}`);
571
622
  }
572
623
 
624
+ // The front door should steer, not trap. For a plain-language ask, point to
625
+ // the better native door when the pattern is obvious, then still run the turn.
626
+ function showRouteCoach(input) {
627
+ try {
628
+ const advice = routeCoach(input, {
629
+ route,
630
+ harnesses,
631
+ hasIntentDir: intentFiles.length > 0,
632
+ turnsThisSession: Math.floor(messages.length / 2),
633
+ });
634
+ if (!advice || advice.id === lastRouteCoachId) return;
635
+ lastRouteCoachId = advice.id;
636
+ console.log(` ${peach('↪')} ${sage(advice.message)} ${cream(advice.command)} ${slate('· still sending this turn')}`);
637
+ } catch { /* route coaching is advisory, never a blocker */ }
638
+ }
639
+
573
640
  // Open a known project from the bootstrap menu: chdir, reload memory,
574
641
  // back to the normal flow. The session is the cockpit; projects swap in.
575
642
  function openProjectAt(dir) {
@@ -669,6 +736,7 @@ async function main() {
669
736
  lastTurnFailure = null;
670
737
  awaitingOutcome = decisionId;
671
738
  console.log(slate(` via ${HARNESSES[harnessId].label} · how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing`));
739
+ showInlineTip((tip) => tip.id === 'capture-intent');
672
740
  return true;
673
741
  } catch (err) {
674
742
  turnInFlight = false;
@@ -719,6 +787,7 @@ async function main() {
719
787
  completionTokens: result.completionTokens,
720
788
  accessToken: config.supabaseAccessToken,
721
789
  });
790
+ showInlineTip((tip) => tip.id === 'capture-intent');
722
791
  return true;
723
792
  } catch (err) {
724
793
  turnInFlight = false;
@@ -1040,6 +1109,16 @@ async function main() {
1040
1109
  // Any typed input supersedes a pending "did you mean" offer.
1041
1110
  if (pendingDidYouMean) pendingDidYouMean = null;
1042
1111
 
1112
+ // Exact bare words are front-door controls too. Keep this exact-match only:
1113
+ // "help" opens help; "help me build X" still routes as natural language.
1114
+ if (!input.startsWith('/')) {
1115
+ const bare = input.trim().toLowerCase();
1116
+ if (BARE_COMMANDS[bare]) {
1117
+ await handleInput(BARE_COMMANDS[bare]);
1118
+ return;
1119
+ }
1120
+ }
1121
+
1043
1122
  // A bare number right after a route failure picks the fallback
1044
1123
  if (awaitingFallback) {
1045
1124
  const af = awaitingFallback;
@@ -1565,15 +1644,14 @@ async function main() {
1565
1644
  }
1566
1645
 
1567
1646
  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
1647
  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' });
1648
+ const { spawnSync } = require('child_process');
1649
+ const args = ['clarify'];
1650
+ if (cmdArg) args.push('--text', cmdArg);
1651
+ const res = spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), ...args], { stdio: 'inherit' });
1652
+ if (res.status !== 0) {
1653
+ console.error(` ${ember('!')} ${sage('Clarify exited without writing context.')}`);
1654
+ }
1577
1655
  intentFiles = loadIntentContext();
1578
1656
  systemPrompt = buildSystemPrompt(intentFiles);
1579
1657
  if (intentFiles.length > 0) {
@@ -2092,7 +2170,7 @@ async function main() {
2092
2170
  }
2093
2171
  if (!h.installed) continue; // hide the long tail of uninstalled extras
2094
2172
  const via = h.headless ? '' : ' · /work only';
2095
- rows.push([h.label, `ready — ${h.role}${via}`, 'green']);
2173
+ rows.push([h.label, `ready — ${h.bestFor || h.role}${via}`, 'green']);
2096
2174
  }
2097
2175
  rows.push(['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '... (' + (config.provider || 'anthropic') + ')' : 'not set — optional', config?.apiKey ? 'green' : 'yellow']);
2098
2176
  rows.push(['Fallback', (config?.fallback === 'auto' ? 'auto-switch on failure' : 'ask before switching') + ' — /fallback to change', 'peach']);
@@ -2184,7 +2262,7 @@ async function main() {
2184
2262
  const mode = h.headless ? '' : slate(' · /work only');
2185
2263
  const badge = hStats ? learning.keptBadge(hStats, h.id) : '';
2186
2264
  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}`);
2265
+ console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.bestFor || h.role || h.label).padEnd(38))} ${slate(h.label)}${mode}${rec}${active}`);
2188
2266
  }
2189
2267
  ui.divider('line');
2190
2268
  const learned = hStats ? learning.learningLine(hStats) : null;
@@ -2420,8 +2498,10 @@ async function main() {
2420
2498
  // Regular input → route it (harness = your subscription, api = your key)
2421
2499
  if (!route) {
2422
2500
  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.')}`);
2501
+ console.log(` ${sage('That request is valid phewsh just needs an AI worker behind the door.')}`);
2502
+ console.log(` ${cream('phewsh setup')} ${sage('detects installed tools and picks a route')}`);
2503
+ console.log(` ${cream('/key')} ${sage('adds an API key, or install Claude Code / Codex / Gemini and rerun phewsh')}`);
2504
+ console.log(` ${slate('Once a route exists, plain typing works; no slash command needed.')}`);
2425
2505
  console.log('');
2426
2506
  rl.prompt();
2427
2507
  return;
@@ -2452,6 +2532,8 @@ async function main() {
2452
2532
  return;
2453
2533
  }
2454
2534
 
2535
+ showRouteCoach(input);
2536
+
2455
2537
  const ok = route.type === 'harness'
2456
2538
  ? await runHarnessTurn(input, route.id, fullSystem)
2457
2539
  : await runApiTurn(input, fullSystem);
@@ -2490,6 +2572,14 @@ async function main() {
2490
2572
  console.log(`\n ${sage('session ended')}\n`);
2491
2573
  process.exit(0);
2492
2574
  });
2575
+
2576
+ // Doorway shortcut: `phewsh <harness>` sets PHEWSH_AUTOWORK so we drop the
2577
+ // user straight into /work for that tool after the front door renders.
2578
+ const autoWork = process.env.PHEWSH_AUTOWORK;
2579
+ if (autoWork && HARNESSES[autoWork]) {
2580
+ delete process.env.PHEWSH_AUTOWORK;
2581
+ handleInput('/work ' + autoWork).catch(() => {});
2582
+ }
2493
2583
  }
2494
2584
 
2495
2585
  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,20 @@ 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
37
  };
38
38
 
39
39
  // In-flight harness children — so ESC in the session can cancel a turn.
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.31",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"