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 +6 -0
- package/commands/session.js +103 -13
- package/commands/setup.js +3 -2
- package/lib/harnesses.js +14 -14
- package/lib/intro.js +6 -2
- package/lib/route-coach.js +91 -0
- package/lib/suggest.js +3 -2
- package/lib/ui.js +10 -1
- package/package.json +1 -1
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);
|
package/commands/session.js
CHANGED
|
@@ -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 {
|
|
1575
|
-
const args =
|
|
1576
|
-
|
|
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(
|
|
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('
|
|
2424
|
-
console.log(` ${
|
|
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
|
-
|
|
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
|
-
|
|
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 >=
|
|
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}
|
|
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
|
-
|
|
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
|
|