phewsh 0.15.11 → 0.15.13
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/commands/session.js +154 -4
- package/lib/closest.js +59 -0
- package/lib/suggest.js +111 -0
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -19,6 +19,8 @@ const { readPPS } = require('../lib/pps');
|
|
|
19
19
|
const { push, pull, ensureValidToken } = require('./sync');
|
|
20
20
|
const { HARNESSES, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
|
|
21
21
|
const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
|
|
22
|
+
const { suggest, suggestAll } = require('../lib/suggest');
|
|
23
|
+
const { closest } = require('../lib/closest');
|
|
22
24
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
23
25
|
const configFile = require('../lib/config-file');
|
|
24
26
|
const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
|
|
@@ -330,6 +332,8 @@ async function main() {
|
|
|
330
332
|
let awaitingOutcome = null; // decision id eligible for 1-4 labeling
|
|
331
333
|
let awaitingFallback = null; // { input, fullSystem, options } after a route failure
|
|
332
334
|
let bootstrapChoices = null; // root-bootstrap menu entries when no project here
|
|
335
|
+
let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
|
|
336
|
+
let pendingDidYouMean = null; // a suggested command; bare Enter accepts + runs it
|
|
333
337
|
let decisionsThisSession = 0;
|
|
334
338
|
|
|
335
339
|
// ── The Exhale: animated brand reveal ──────────────────
|
|
@@ -454,6 +458,55 @@ async function main() {
|
|
|
454
458
|
console.log(` ${slate('pick a number, or just type — your context travels with every route')}`);
|
|
455
459
|
}
|
|
456
460
|
|
|
461
|
+
// Self-aware guidance: snapshot the session's state so phewsh can recommend
|
|
462
|
+
// the one next step worth taking, instead of leaving the user to know commands.
|
|
463
|
+
function buildSuggestState() {
|
|
464
|
+
let seqStale = false;
|
|
465
|
+
try {
|
|
466
|
+
const cwd = process.cwd();
|
|
467
|
+
const claudePath = path.join(cwd, 'CLAUDE.md');
|
|
468
|
+
const intentDir = path.join(cwd, '.intent');
|
|
469
|
+
if (fs.existsSync(claudePath) && fs.existsSync(intentDir)) {
|
|
470
|
+
const claudeT = fs.statSync(claudePath).mtimeMs;
|
|
471
|
+
const newestIntent = fs.readdirSync(intentDir)
|
|
472
|
+
.filter(f => f.endsWith('.md') || f.endsWith('.json'))
|
|
473
|
+
.reduce((m, f) => Math.max(m, fs.statSync(path.join(intentDir, f)).mtimeMs), 0);
|
|
474
|
+
seqStale = newestIntent > claudeT + 1000; // >1s newer = drift
|
|
475
|
+
}
|
|
476
|
+
} catch { /* drift detection is best-effort */ }
|
|
477
|
+
|
|
478
|
+
let ambientOn = false;
|
|
479
|
+
try {
|
|
480
|
+
const s = fs.readFileSync(path.join(os.homedir(), '.claude', 'settings.json'), 'utf-8');
|
|
481
|
+
ambientOn = s.includes('phewsh hook session-start');
|
|
482
|
+
} catch { /* no settings = ambient off */ }
|
|
483
|
+
|
|
484
|
+
let pending = 0;
|
|
485
|
+
try { pending = pendingDecisions({ project: projectName }).length; } catch { /* best-effort */ }
|
|
486
|
+
|
|
487
|
+
let installed = [];
|
|
488
|
+
try { installed = listHarnesses().filter(h => h.installed).map(h => h.id); } catch { /* best-effort */ }
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
hasIntentDir: fs.existsSync(path.join(process.cwd(), '.intent')),
|
|
492
|
+
intentFileCount: intentFiles.length,
|
|
493
|
+
pendingOutcomes: pending,
|
|
494
|
+
installedHarnesses: installed,
|
|
495
|
+
route,
|
|
496
|
+
turnsThisSession: Math.floor(messages.length / 2),
|
|
497
|
+
seqStale,
|
|
498
|
+
ambientOn,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// One subtle line under the menu — the single highest-leverage nudge, if any.
|
|
503
|
+
function showInlineTip() {
|
|
504
|
+
let tip = null;
|
|
505
|
+
try { tip = suggest(buildSuggestState()); } catch { /* never block the prompt on guidance */ }
|
|
506
|
+
if (!tip) return;
|
|
507
|
+
console.log(` ${teal('⤷')} ${sage(tip.message)} ${cream(tip.command.trim())} ${slate('· /next for options')}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
457
510
|
// Open a known project from the bootstrap menu: chdir, reload memory,
|
|
458
511
|
// back to the normal flow. The session is the cockpit; projects swap in.
|
|
459
512
|
function openProjectAt(dir) {
|
|
@@ -470,6 +523,7 @@ async function main() {
|
|
|
470
523
|
console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(`.intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''} loaded`)} ${slate('· via ' + routeLabel(route, config))}`);
|
|
471
524
|
console.log('');
|
|
472
525
|
showModeMenu();
|
|
526
|
+
showInlineTip();
|
|
473
527
|
console.log('');
|
|
474
528
|
}
|
|
475
529
|
|
|
@@ -502,6 +556,7 @@ async function main() {
|
|
|
502
556
|
showBootstrapMenu(recents);
|
|
503
557
|
} else {
|
|
504
558
|
showModeMenu();
|
|
559
|
+
showInlineTip();
|
|
505
560
|
}
|
|
506
561
|
console.log('');
|
|
507
562
|
|
|
@@ -696,7 +751,8 @@ async function main() {
|
|
|
696
751
|
'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'run',
|
|
697
752
|
'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
|
|
698
753
|
'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
|
|
699
|
-
'agents', 'context', 'gate', 'reload', 'sequence', 'setup', 'system', 'watch',
|
|
754
|
+
'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
755
|
+
'next', 'recommend', 'guide',
|
|
700
756
|
]);
|
|
701
757
|
const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
|
|
702
758
|
let turnAbort = null; // AbortController while an API turn streams
|
|
@@ -860,6 +916,9 @@ async function main() {
|
|
|
860
916
|
async function handleInput(input) {
|
|
861
917
|
input = expandPastes(input);
|
|
862
918
|
|
|
919
|
+
// Any typed input supersedes a pending "did you mean" offer.
|
|
920
|
+
if (pendingDidYouMean) pendingDidYouMean = null;
|
|
921
|
+
|
|
863
922
|
// A bare number right after a route failure picks the fallback
|
|
864
923
|
if (awaitingFallback) {
|
|
865
924
|
const af = awaitingFallback;
|
|
@@ -892,6 +951,29 @@ async function main() {
|
|
|
892
951
|
return;
|
|
893
952
|
}
|
|
894
953
|
|
|
954
|
+
// /next pick: a bare number runs the chosen suggestion (slash = in place).
|
|
955
|
+
// Any non-digit input means the user moved on — drop the offer so it never
|
|
956
|
+
// shadows a later mode pick.
|
|
957
|
+
if (nextChoices && !/^[0-9]$/.test(input)) nextChoices = null;
|
|
958
|
+
if (nextChoices && /^[0-9]$/.test(input)) {
|
|
959
|
+
const pick = nextChoices[parseInt(input, 10) - 1];
|
|
960
|
+
if (!pick) {
|
|
961
|
+
console.log(` ${sage('Pick 1-' + nextChoices.length + ', or keep typing')}`);
|
|
962
|
+
rl.prompt();
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const command = pick.command.trim();
|
|
966
|
+
nextChoices = null;
|
|
967
|
+
if (command.startsWith('/')) {
|
|
968
|
+
await handleInput(command); // re-dispatch the slash command in place
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
console.log(` ${teal('⤷')} ${sage('run this in your shell:')} ${cream(command)}`);
|
|
972
|
+
console.log('');
|
|
973
|
+
rl.prompt();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
895
977
|
// Root bootstrap: a bare number opens a project, inits, or scans
|
|
896
978
|
if (bootstrapChoices && messages.length === 0 && /^[0-9]{1,2}$/.test(input)) {
|
|
897
979
|
const choice = bootstrapChoices[parseInt(input, 10) - 1];
|
|
@@ -996,10 +1078,63 @@ async function main() {
|
|
|
996
1078
|
process.exit(0);
|
|
997
1079
|
}
|
|
998
1080
|
|
|
1081
|
+
// ── /next ──────────────────────────────────────────
|
|
1082
|
+
// The self-aware "what should I do?" button: phewsh reads the session's
|
|
1083
|
+
// state and hands back ranked, pickable next steps — no command to recall.
|
|
1084
|
+
if (cmd === 'next' || cmd === 'recommend' || cmd === 'guide') {
|
|
1085
|
+
let ranked = [];
|
|
1086
|
+
try { ranked = suggestAll(buildSuggestState()); } catch { /* best-effort */ }
|
|
1087
|
+
console.log('');
|
|
1088
|
+
if (ranked.length === 0) {
|
|
1089
|
+
console.log(` ${teal('●')} ${sage("You're aligned — nothing pressing. Keep working, or /help for everything.")}`);
|
|
1090
|
+
console.log('');
|
|
1091
|
+
nextChoices = null;
|
|
1092
|
+
rl.prompt();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
console.log(` ${b(cream('What would move you forward'))} ${slate('— pick a number, or ignore me')}`);
|
|
1096
|
+
console.log('');
|
|
1097
|
+
nextChoices = ranked.slice(0, 3);
|
|
1098
|
+
nextChoices.forEach((s, i) => {
|
|
1099
|
+
console.log(` ${teal(String(i + 1))} ${cream(s.command.trim())} ${sage(s.message)}`);
|
|
1100
|
+
console.log(` ${slate(s.why)}`);
|
|
1101
|
+
});
|
|
1102
|
+
console.log('');
|
|
1103
|
+
console.log(` ${slate('a slash command runs in place; anything else, I show you the line to run')}`);
|
|
1104
|
+
console.log('');
|
|
1105
|
+
rl.prompt();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
999
1109
|
if (cmd === 'help' || cmd === 'h') {
|
|
1110
|
+
const wantsAll = /^(all|more|full|everything)$/i.test(cmdArg.trim());
|
|
1111
|
+
|
|
1112
|
+
// Bare /help → the short essentials a newcomer actually needs.
|
|
1113
|
+
if (!wantsAll) {
|
|
1114
|
+
console.log('');
|
|
1115
|
+
console.log(` ${sage('the loop: define .intent/ → sync → work → evolve → repeat')}`);
|
|
1116
|
+
console.log('');
|
|
1117
|
+
console.log(` ${cream('the essentials')}`);
|
|
1118
|
+
console.log(` ${teal('just type')} ${sage('chat — your .intent/ context travels with every route')}`);
|
|
1119
|
+
console.log(` ${teal('/next')} ${sage('not sure? phewsh names the next step worth taking')}`);
|
|
1120
|
+
console.log(` ${teal('/use')} ${slate('<tool>')} ${sage('route through Claude Code, Codex, Gemini… (their login, no key)')}`);
|
|
1121
|
+
console.log(` ${teal('@name')} ${slate('<msg>')} ${sage('one message to one tool — @codex review this')}`);
|
|
1122
|
+
console.log(` ${teal('/work')} ${slate('[tool]')} ${sage('hand off to the full interactive tool, outcome on return')}`);
|
|
1123
|
+
console.log(` ${teal('/clarify')} ${sage('turn messy thoughts into .intent/ artifacts')}`);
|
|
1124
|
+
console.log(` ${teal('/outcomes')} ${sage('label what you kept — the record that gets smarter')}`);
|
|
1125
|
+
console.log('');
|
|
1126
|
+
console.log(` ${slate('/help all')} ${sage('everything')} ${slate('·')} ${slate('/tour')} ${sage('walkthrough')} ${slate('·')} ${slate('/quit')} ${sage('exit')}`);
|
|
1127
|
+
console.log('');
|
|
1128
|
+
rl.prompt();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1000
1132
|
console.log('');
|
|
1001
1133
|
console.log(` ${sage('the loop: define .intent/ → sync → work → evolve → repeat')}`);
|
|
1002
1134
|
console.log('');
|
|
1135
|
+
console.log(` ${cream('not sure what to do?')}`);
|
|
1136
|
+
console.log(` ${teal('/next')} ${sage("phewsh reads your state and hands back the next step worth taking")}`);
|
|
1137
|
+
console.log('');
|
|
1003
1138
|
console.log(` ${cream('author .intent/')}`);
|
|
1004
1139
|
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
1005
1140
|
console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
|
|
@@ -1936,8 +2071,14 @@ async function main() {
|
|
|
1936
2071
|
return;
|
|
1937
2072
|
}
|
|
1938
2073
|
|
|
1939
|
-
// Unknown slash command
|
|
1940
|
-
|
|
2074
|
+
// Unknown slash command — suggest the nearest real one instead of a dead end.
|
|
2075
|
+
const guess = closest(cmd, [...KNOWN_COMMANDS]);
|
|
2076
|
+
if (guess) {
|
|
2077
|
+
console.log(` ${sage('No command')} ${cream('/' + cmd)}${sage('. Did you mean')} ${teal('/' + guess)}${sage('?')} ${slate('· enter = run it · /help all for everything')}`);
|
|
2078
|
+
pendingDidYouMean = '/' + guess + (cmdArg ? ' ' + cmdArg : '');
|
|
2079
|
+
} else {
|
|
2080
|
+
console.log(` ${sage('No command')} ${cream('/' + cmd)}${sage('.')} ${slate('Type')} ${teal('/next')} ${slate('for what to do, or')} ${teal('/help')} ${slate('for everything.')}`);
|
|
2081
|
+
}
|
|
1941
2082
|
rl.prompt();
|
|
1942
2083
|
return;
|
|
1943
2084
|
}
|
|
@@ -1991,7 +2132,16 @@ async function main() {
|
|
|
1991
2132
|
|
|
1992
2133
|
const lineDispatcher = createLineDispatcher(handleInput, {
|
|
1993
2134
|
onBatch: ({ input, lines }) => collapsePastedEcho(lines, input),
|
|
1994
|
-
onNoop: () =>
|
|
2135
|
+
onNoop: () => {
|
|
2136
|
+
// Bare Enter accepts a pending "did you mean" suggestion.
|
|
2137
|
+
if (pendingDidYouMean) {
|
|
2138
|
+
const cmd = pendingDidYouMean;
|
|
2139
|
+
pendingDidYouMean = null;
|
|
2140
|
+
handleInput(cmd).catch(() => {});
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
rl.prompt();
|
|
2144
|
+
},
|
|
1995
2145
|
onError: (err) => {
|
|
1996
2146
|
console.error(`\n ${ember('!')} ${sage('Input failed:')} ${err.message}`);
|
|
1997
2147
|
rl.prompt();
|
package/lib/closest.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Find the nearest known command to a typo — so an unknown slash command
|
|
2
|
+
// becomes "did you mean /clarify?" instead of a dead end. Pure + deterministic.
|
|
3
|
+
//
|
|
4
|
+
// Strategy, in order of confidence:
|
|
5
|
+
// 1. Exact prefix — user typed a real abbreviation (/cla → /clarify).
|
|
6
|
+
// 2. Small edit distance — a genuine typo (/claify → /clarify).
|
|
7
|
+
// Returns the single best candidate, or null when nothing is close enough.
|
|
8
|
+
|
|
9
|
+
function levenshtein(a, b) {
|
|
10
|
+
if (a === b) return 0;
|
|
11
|
+
if (!a.length) return b.length;
|
|
12
|
+
if (!b.length) return a.length;
|
|
13
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
14
|
+
let curr = new Array(b.length + 1);
|
|
15
|
+
for (let i = 1; i <= a.length; i++) {
|
|
16
|
+
curr[0] = i;
|
|
17
|
+
for (let j = 1; j <= b.length; j++) {
|
|
18
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
19
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
20
|
+
}
|
|
21
|
+
[prev, curr] = [curr, prev];
|
|
22
|
+
}
|
|
23
|
+
return prev[b.length];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} input — the unknown token (no leading slash)
|
|
28
|
+
* @param {string[]} candidates — known command names
|
|
29
|
+
* @param {object} [opts]
|
|
30
|
+
* @param {number} [opts.maxDistance=2] — max edit distance to still suggest
|
|
31
|
+
* @returns {string|null} best candidate or null
|
|
32
|
+
*/
|
|
33
|
+
function closest(input, candidates, opts = {}) {
|
|
34
|
+
const maxDistance = opts.maxDistance ?? 2;
|
|
35
|
+
if (!input || !candidates || candidates.length === 0) return null;
|
|
36
|
+
const q = input.toLowerCase();
|
|
37
|
+
|
|
38
|
+
// 1. Prefix match — shortest matching command wins (most specific abbrev).
|
|
39
|
+
// Require at least 2 chars so a stray "/a" doesn't latch onto everything.
|
|
40
|
+
if (q.length >= 2) {
|
|
41
|
+
const prefixes = candidates
|
|
42
|
+
.filter(c => c.toLowerCase().startsWith(q))
|
|
43
|
+
.sort((a, b) => a.length - b.length);
|
|
44
|
+
if (prefixes.length) return prefixes[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Edit distance — nearest within threshold; scale threshold down for
|
|
48
|
+
// very short inputs so "x" doesn't "match" every 1-2 char command.
|
|
49
|
+
const cap = Math.min(maxDistance, Math.max(1, Math.floor(q.length / 2) + 1));
|
|
50
|
+
let best = null;
|
|
51
|
+
let bestD = Infinity;
|
|
52
|
+
for (const c of candidates) {
|
|
53
|
+
const d = levenshtein(q, c.toLowerCase());
|
|
54
|
+
if (d < bestD) { bestD = d; best = c; }
|
|
55
|
+
}
|
|
56
|
+
return bestD <= cap ? best : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { closest, levenshtein };
|
package/lib/suggest.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Self-aware guidance — phewsh watching the user's state and surfacing the
|
|
2
|
+
// single most useful next step, so nobody has to memorize commands like
|
|
3
|
+
// `phewsh seq --dry-run`. Pure and deterministic: feed it a state snapshot,
|
|
4
|
+
// get back ranked suggestions. The session layer renders + offers them.
|
|
5
|
+
//
|
|
6
|
+
// Each suggestion: { id, priority, message, command, why }
|
|
7
|
+
// message — one plain line, what's true right now (the caller colorizes)
|
|
8
|
+
// command — the exact thing to run (a slash command or shell command)
|
|
9
|
+
// why — one short clause: why it helps (shown on /next, not inline)
|
|
10
|
+
//
|
|
11
|
+
// Design rules:
|
|
12
|
+
// - Never nag: a suggestion only fires when its trigger is genuinely met.
|
|
13
|
+
// - One at a time inline; up to a few on demand via /next.
|
|
14
|
+
// - Ordered by leverage — capture-intent and learn-from-outcomes first,
|
|
15
|
+
// because those are the moat; convenience nudges last.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} s state snapshot
|
|
19
|
+
* @param {boolean} s.hasIntentDir — .intent/ exists in cwd
|
|
20
|
+
* @param {number} s.intentFileCount — .intent/ files loaded
|
|
21
|
+
* @param {number} s.pendingOutcomes — unlabeled decisions (this project)
|
|
22
|
+
* @param {string[]} s.installedHarnesses — harness ids present on the machine
|
|
23
|
+
* @param {string[]} [s.usedRoutes] — harness ids used this session
|
|
24
|
+
* @param {string|null} s.route — current route id
|
|
25
|
+
* @param {number} s.turnsThisSession — exchanges so far
|
|
26
|
+
* @param {boolean} s.seqStale — .intent/ newer than CLAUDE.md (drift)
|
|
27
|
+
* @param {boolean} s.ambientOn — ambient hooks installed
|
|
28
|
+
* @returns {Array} ranked suggestions (highest leverage first)
|
|
29
|
+
*/
|
|
30
|
+
function suggestAll(s = {}) {
|
|
31
|
+
const {
|
|
32
|
+
hasIntentDir = false,
|
|
33
|
+
intentFileCount = 0,
|
|
34
|
+
pendingOutcomes = 0,
|
|
35
|
+
installedHarnesses = [],
|
|
36
|
+
usedRoutes = [],
|
|
37
|
+
route = null,
|
|
38
|
+
turnsThisSession = 0,
|
|
39
|
+
seqStale = false,
|
|
40
|
+
ambientOn = false,
|
|
41
|
+
} = s;
|
|
42
|
+
|
|
43
|
+
const out = [];
|
|
44
|
+
|
|
45
|
+
// 1. Working without captured intent — the biggest miss. Every AI tool here
|
|
46
|
+
// could be reading the same goal; right now none are.
|
|
47
|
+
if (intentFileCount === 0 && turnsThisSession >= 2) {
|
|
48
|
+
out.push({
|
|
49
|
+
id: 'capture-intent',
|
|
50
|
+
priority: 100,
|
|
51
|
+
message: `${turnsThisSession} messages in, no captured intent — every AI here is guessing.`,
|
|
52
|
+
command: '/clarify',
|
|
53
|
+
why: 'Turns what you just said into a spec every tool (this one, Claude Code, Codex) reads.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Decisions piling up unlabeled — the record only gets smarter if labeled.
|
|
58
|
+
if (pendingOutcomes >= 3) {
|
|
59
|
+
out.push({
|
|
60
|
+
id: 'label-outcomes',
|
|
61
|
+
priority: 90,
|
|
62
|
+
message: `${pendingOutcomes} decisions unlabeled — phewsh can't learn what you keep until you say.`,
|
|
63
|
+
command: '/outcomes',
|
|
64
|
+
why: 'Labeling kept/reverted is the dataset; it weights future routing and recall.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. .intent/ drifted from CLAUDE.md — the editor's AI is reading stale context.
|
|
69
|
+
if (hasIntentDir && seqStale) {
|
|
70
|
+
out.push({
|
|
71
|
+
id: 'resync-claude-md',
|
|
72
|
+
priority: 80,
|
|
73
|
+
message: `Your .intent/ changed but CLAUDE.md didn't — editor AIs are reading stale context.`,
|
|
74
|
+
command: '/seq claude',
|
|
75
|
+
why: 'Re-sequences the latest intent + memory into CLAUDE.md so every tool sees today.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Multiple harnesses installed, only leaning on one — council is free leverage.
|
|
80
|
+
const others = installedHarnesses.filter(h => h !== route);
|
|
81
|
+
if (installedHarnesses.length >= 2 && turnsThisSession >= 3 && others.length >= 1) {
|
|
82
|
+
out.push({
|
|
83
|
+
id: 'try-council',
|
|
84
|
+
priority: 60,
|
|
85
|
+
message: `You have ${installedHarnesses.length} agents installed but lean on one — the rest are idle.`,
|
|
86
|
+
command: '/council ',
|
|
87
|
+
why: 'Asks every installed harness in parallel; you keep the best answer, context shared.',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. Ambient off — continuity that costs nothing once turned on.
|
|
92
|
+
if (hasIntentDir && !ambientOn && installedHarnesses.includes('claude-code')) {
|
|
93
|
+
out.push({
|
|
94
|
+
id: 'enable-ambient',
|
|
95
|
+
priority: 50,
|
|
96
|
+
message: `Ambient is off — Claude Code sessions here won't get the brief unless you launch phewsh.`,
|
|
97
|
+
command: 'phewsh ambient',
|
|
98
|
+
why: 'Installs a silent SessionStart hook so continuity travels even when you forget phewsh.',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return out.sort((a, b) => b.priority - a.priority);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The single highest-leverage suggestion, or null. */
|
|
106
|
+
function suggest(s = {}) {
|
|
107
|
+
const all = suggestAll(s);
|
|
108
|
+
return all.length ? all[0] : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { suggest, suggestAll };
|