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 +6 -0
- package/commands/session.js +143 -18
- package/commands/setup.js +3 -2
- package/lib/harnesses.js +36 -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
|
@@ -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 {
|
|
1575
|
-
const args =
|
|
1576
|
-
|
|
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(
|
|
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
|
-
|
|
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\
|
|
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('
|
|
2317
|
-
console.log(` ${sage('Record:')} ${savedBrief.written ? slate('exact briefing saved locally
|
|
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('
|
|
2424
|
-
console.log(` ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|