phewsh 0.15.25 → 0.15.27
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 +47 -3
- package/lib/outcomes.js +4 -1
- package/lib/suggest.js +13 -0
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -334,6 +334,7 @@ async function main() {
|
|
|
334
334
|
let route = resolveRoute(config, harnesses);
|
|
335
335
|
let sessionMode = null; // INTENT_MODES id once picked
|
|
336
336
|
let awaitingOutcome = null; // decision id eligible for 1-4 labeling
|
|
337
|
+
let awaitingWhy = null; // { id, outcome } — next line is the reason
|
|
337
338
|
let awaitingFallback = null; // { input, fullSystem, options } after a route failure
|
|
338
339
|
let bootstrapChoices = null; // root-bootstrap menu entries when no project here
|
|
339
340
|
let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
|
|
@@ -491,15 +492,22 @@ async function main() {
|
|
|
491
492
|
let installed = [];
|
|
492
493
|
try { installed = listHarnesses().filter(h => h.installed).map(h => h.id); } catch { /* best-effort */ }
|
|
493
494
|
|
|
495
|
+
let bestKeeper = null;
|
|
496
|
+
try {
|
|
497
|
+
const br = learning.bestRoute(outcomeStats({ project: projectName }), { minSample: 4 });
|
|
498
|
+
if (br) bestKeeper = { route: br.route, label: continuity.labelFor(br.route), keptRate: br.keptRate, total: br.total };
|
|
499
|
+
} catch { /* best-effort */ }
|
|
500
|
+
|
|
494
501
|
return {
|
|
495
502
|
hasIntentDir: fs.existsSync(path.join(process.cwd(), '.intent')),
|
|
496
503
|
intentFileCount: intentFiles.length,
|
|
497
504
|
pendingOutcomes: pending,
|
|
498
505
|
installedHarnesses: installed,
|
|
499
|
-
route,
|
|
506
|
+
route: route?.id || route,
|
|
500
507
|
turnsThisSession: Math.floor(messages.length / 2),
|
|
501
508
|
seqStale,
|
|
502
509
|
ambientOn,
|
|
510
|
+
bestKeeper,
|
|
503
511
|
};
|
|
504
512
|
}
|
|
505
513
|
|
|
@@ -597,6 +605,7 @@ async function main() {
|
|
|
597
605
|
if (s.length > 50) s = s.slice(0, 49).trimEnd() + '…';
|
|
598
606
|
const verb = hit.outcome === 'failed' ? 'failed' : 'reverted';
|
|
599
607
|
console.log(` ${peach('↩')} ${sage(`You ${verb} something close before:`)} ${slate('“' + s + '” · via ' + continuity.labelFor(hit.route) + ' · ' + continuity.agoText(hit.ts))}`);
|
|
608
|
+
if (hit.why) console.log(` ${slate(' why it didn\'t hold: ')}${sage(hit.why)}`);
|
|
600
609
|
console.log(` ${slate(' not a block — just so the record doesn\'t let you repeat it blind.')}`);
|
|
601
610
|
} catch { /* recall is advisory, never blocks a turn */ }
|
|
602
611
|
}
|
|
@@ -955,6 +964,16 @@ async function main() {
|
|
|
955
964
|
async function handleInput(input) {
|
|
956
965
|
input = expandPastes(input);
|
|
957
966
|
|
|
967
|
+
// Answering the "why?" prompt — the whole line is the reason, not a command.
|
|
968
|
+
if (awaitingWhy) {
|
|
969
|
+
const { id, outcome } = awaitingWhy;
|
|
970
|
+
awaitingWhy = null;
|
|
971
|
+
try { labelOutcome(id, outcome, input); console.log(` ${slate('noted — the record will remember why.')}`); } catch { /* gone */ }
|
|
972
|
+
console.log('');
|
|
973
|
+
rl.prompt();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
958
977
|
// Any typed input supersedes a pending "did you mean" offer.
|
|
959
978
|
if (pendingDidYouMean) pendingDidYouMean = null;
|
|
960
979
|
|
|
@@ -979,12 +998,22 @@ async function main() {
|
|
|
979
998
|
// A bare 1-4 right after a routed action labels its outcome
|
|
980
999
|
if (awaitingOutcome && /^[1-4]$/.test(input)) {
|
|
981
1000
|
const outcome = OUTCOMES[parseInt(input, 10) - 1];
|
|
1001
|
+
let labeled = null;
|
|
982
1002
|
try {
|
|
983
|
-
labelOutcome(awaitingOutcome, outcome);
|
|
1003
|
+
labeled = labelOutcome(awaitingOutcome, outcome);
|
|
984
1004
|
const color = outcome === 'kept' ? green : outcome === 'superseded' ? peach : ember;
|
|
985
1005
|
console.log(` ${teal('●')} ${sage('outcome:')} ${color(outcome)}`);
|
|
986
1006
|
} catch { /* decision vanished — nothing to do */ }
|
|
1007
|
+
const id = awaitingOutcome;
|
|
987
1008
|
awaitingOutcome = null;
|
|
1009
|
+
// For a regret, the reason is the gold — capture one line so /recall can
|
|
1010
|
+
// tell you *why* next time. Kept/superseded stay frictionless.
|
|
1011
|
+
if (labeled && (outcome === 'reverted' || outcome === 'failed')) {
|
|
1012
|
+
awaitingWhy = { id, outcome };
|
|
1013
|
+
console.log(` ${slate('why? one line — feeds /recall next time · enter to skip')}`);
|
|
1014
|
+
rl.prompt();
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
988
1017
|
console.log('');
|
|
989
1018
|
rl.prompt();
|
|
990
1019
|
return;
|
|
@@ -1072,10 +1101,23 @@ async function main() {
|
|
|
1072
1101
|
if (n === 5) {
|
|
1073
1102
|
console.log('');
|
|
1074
1103
|
console.log(` ${b(cream('Ask another model — switch the route'))}`);
|
|
1104
|
+
let mStats = null, best = null;
|
|
1105
|
+
try {
|
|
1106
|
+
mStats = outcomeStats({ project: projectName });
|
|
1107
|
+
best = learning.bestRoute(mStats, { minSample: 3 });
|
|
1108
|
+
} catch { /* best-effort */ }
|
|
1109
|
+
// Only celebrate a route that actually holds up (≥50% kept) — a star on
|
|
1110
|
+
// a poor rate would be dishonest. The badge shows regardless.
|
|
1111
|
+
const starRoute = best && best.keptRate >= 0.5 ? best.route : null;
|
|
1075
1112
|
for (const h of harnesses) {
|
|
1076
|
-
if (h.installed)
|
|
1113
|
+
if (!h.installed) continue;
|
|
1114
|
+
const badge = mStats ? learning.keptBadge(mStats, h.id) : '';
|
|
1115
|
+
const star = starRoute === h.id ? ` ${green('★ keeps best')}` : '';
|
|
1116
|
+
const rec = badge ? ` ${slate('· ' + badge)}` : '';
|
|
1117
|
+
console.log(` ${teal('/use ' + h.id.padEnd(12))} ${sage(h.label)} ${slate('(' + h.auth + ')')}${rec}${star}`);
|
|
1077
1118
|
}
|
|
1078
1119
|
if (config?.apiKey) console.log(` ${teal('/use api'.padEnd(17))} ${sage('Direct API')} ${slate('(your key)')}`);
|
|
1120
|
+
if (starRoute) console.log(` ${slate('★ = highest kept-rate in your record so far')}`);
|
|
1079
1121
|
console.log('');
|
|
1080
1122
|
rl.prompt();
|
|
1081
1123
|
return;
|
|
@@ -2268,6 +2310,8 @@ async function main() {
|
|
|
2268
2310
|
const lineDispatcher = createLineDispatcher(handleInput, {
|
|
2269
2311
|
onBatch: ({ input, lines }) => { collapsePastedEcho(lines, input); cmdHistory.append(input); },
|
|
2270
2312
|
onNoop: () => {
|
|
2313
|
+
// Bare Enter skips the "why?" prompt — the label still stands.
|
|
2314
|
+
if (awaitingWhy) { awaitingWhy = null; console.log(''); rl.prompt(); return; }
|
|
2271
2315
|
// Bare Enter accepts a pending "did you mean" suggestion.
|
|
2272
2316
|
if (pendingDidYouMean) {
|
|
2273
2317
|
const cmd = pendingDidYouMean;
|
package/lib/outcomes.js
CHANGED
|
@@ -51,7 +51,7 @@ function recordDecision({ project, route, mode, summary }) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Label a decision by id (or unambiguous id prefix). Returns the decision or null. */
|
|
54
|
-
function labelOutcome(idOrPrefix, outcome) {
|
|
54
|
+
function labelOutcome(idOrPrefix, outcome, why = null) {
|
|
55
55
|
if (!OUTCOMES.includes(outcome)) {
|
|
56
56
|
throw new Error(`Outcome must be one of: ${OUTCOMES.join(', ')}`);
|
|
57
57
|
}
|
|
@@ -60,6 +60,9 @@ function labelOutcome(idOrPrefix, outcome) {
|
|
|
60
60
|
if (matches.length !== 1) return null;
|
|
61
61
|
matches[0].outcome = outcome;
|
|
62
62
|
matches[0].labeledAt = new Date().toISOString();
|
|
63
|
+
// One-line reason — the most valuable thing about a reverted/failed call.
|
|
64
|
+
// Feeds /recall ("you reverted this before — because …") and /learn.
|
|
65
|
+
if (why && String(why).trim()) matches[0].why = String(why).trim().slice(0, 200);
|
|
63
66
|
save(decisions);
|
|
64
67
|
return matches[0];
|
|
65
68
|
}
|
package/lib/suggest.js
CHANGED
|
@@ -38,6 +38,7 @@ function suggestAll(s = {}) {
|
|
|
38
38
|
turnsThisSession = 0,
|
|
39
39
|
seqStale = false,
|
|
40
40
|
ambientOn = false,
|
|
41
|
+
bestKeeper = null, // { route, label, keptRate, total } from the record, or null
|
|
41
42
|
} = s;
|
|
42
43
|
|
|
43
44
|
const out = [];
|
|
@@ -88,6 +89,18 @@ function suggestAll(s = {}) {
|
|
|
88
89
|
});
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// 4b. The record has a clear favorite that isn't your current route —
|
|
93
|
+
// auto-weighting: lean on what actually holds for you.
|
|
94
|
+
if (bestKeeper && bestKeeper.route !== route && bestKeeper.total >= 4 && bestKeeper.keptRate >= 0.6) {
|
|
95
|
+
out.push({
|
|
96
|
+
id: 'prefer-best-route',
|
|
97
|
+
priority: 55,
|
|
98
|
+
message: `${bestKeeper.label || bestKeeper.route} keeps best in your record (${Math.round(bestKeeper.keptRate * 100)}%, ${bestKeeper.total} labeled).`,
|
|
99
|
+
command: `/use ${bestKeeper.route}`,
|
|
100
|
+
why: 'The record weights routing — lean on the tool whose work you actually keep.',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
// 5. Ambient off — continuity that costs nothing once turned on.
|
|
92
105
|
if (hasIntentDir && !ambientOn && installedHarnesses.includes('claude-code')) {
|
|
93
106
|
out.push({
|