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.
@@ -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) console.log(` ${teal('/use ' + h.id.padEnd(12))} ${sage(h.label)} ${slate('(' + h.auth + ')')}`);
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.25",
3
+ "version": "0.15.27",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"