phewsh 0.15.28 → 0.15.29

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.
@@ -10,6 +10,8 @@ const {
10
10
  OUTCOMES, recordDecision, labelOutcome,
11
11
  pendingDecisions, recentDecisions, outcomeStats, bypassStats,
12
12
  } = require('../lib/outcomes');
13
+ const learning = require('../lib/learning');
14
+ const continuity = require('../lib/continuity');
13
15
 
14
16
  const { b, teal, peach, sage, slate, cream, ember, green } = ui;
15
17
 
@@ -42,76 +44,108 @@ function showBypasses() {
42
44
  }
43
45
  }
44
46
 
47
+ // Plain-language explanation of the contract — shown every time, because the
48
+ // #1 complaint was "I have no idea what this is for or how to use it."
49
+ function explainDeal() {
50
+ console.log(` ${b(cream('Outcomes'))} ${slate('— phewsh\'s memory of what actually worked')}`);
51
+ ui.divider('line');
52
+ console.log(` ${sage('Every call you route, phewsh logs. Tell it what you')} ${green('kept')} ${sage('vs')} ${ember('threw out')}${sage(',')}`);
53
+ console.log(` ${sage('and it learns which tool to trust for which job — and warns you before')}`);
54
+ console.log(` ${sage('you repeat something that flopped.')} ${slate('That labeled record is the one thing')}`);
55
+ console.log(` ${slate('your chat history doesn\'t have. Two taps now = smarter routing later.')}`);
56
+ }
57
+
58
+ // The payoff, in their own data — so labeling feels like it buys something.
59
+ function showPayoff(stats) {
60
+ const lines = [];
61
+ try {
62
+ const best = learning.bestRoute(stats, { minSample: 2 });
63
+ if (best && best.keptRate >= 0.5) {
64
+ const label = continuity.labelFor(best.route) || best.route;
65
+ lines.push(`${green('●')} ${sage(label + ' keeps best for you — ')}${cream(Math.round(best.keptRate * 100) + '%')} ${sage(`(${best.kept}/${best.total}). phewsh leans there when it routes.`)}`);
66
+ }
67
+ } catch { /* best-effort */ }
68
+ const regrets = stats.reverted + stats.failed;
69
+ if (regrets > 0) {
70
+ lines.push(`${ember('●')} ${sage((regrets === 1 ? '1 call you marked a miss is' : regrets + ' calls you marked misses are') + ' remembered — ')}${cream('/recall')} ${sage('warns you before a repeat.')}`);
71
+ }
72
+ if (lines.length === 0) return;
73
+ console.log('');
74
+ console.log(` ${b(cream('What it already knows'))} ${slate('← this is the payoff')}`);
75
+ for (const l of lines) console.log(` ${l}`);
76
+ if (stats.judged < 4) console.log(` ${slate('judge a few more and clearer patterns surface here.')}`);
77
+ }
78
+
45
79
  function showStats() {
46
80
  const stats = outcomeStats();
47
- const labeled = stats.total - stats.pending;
48
81
 
49
82
  console.log('');
50
- console.log(` ${b(cream('Outcomes'))} ${slate('— what your decisions became')}`);
51
- ui.divider('line');
83
+ explainDeal();
52
84
 
53
85
  if (stats.total === 0) {
54
- console.log(` ${sage('Nothing recorded yet.')}`);
55
- console.log(` ${slate('Work through a phewsh session every routed action records a decision.')}`);
56
- console.log(` ${slate('Label them 1-4 as you learn what held up.')}`);
86
+ console.log('');
87
+ console.log(` ${sage('Nothing logged yet start routing work and it fills in.')}`);
88
+ console.log(` ${slate('In a session: just type, or use /use <tool>. Then tap 1-4 to judge it.')}`);
57
89
  showBypasses();
58
90
  console.log('');
59
91
  return;
60
92
  }
61
93
 
62
- console.log(` ${cream(String(stats.total))} ${sage('decisions')} ${slate('·')} ${cream(String(labeled))} ${sage('labeled')} ${slate('·')} ${cream(String(stats.pending))} ${sage('pending')}`);
63
- if (labeled > 0) {
64
- console.log(` ${green(stats.kept + ' kept')} ${slate('·')} ${ember(stats.reverted + ' reverted')} ${slate('·')} ${peach(stats.superseded + ' superseded')} ${slate('·')} ${ember(stats.failed + ' failed')}`);
94
+ // The state of the record, in plain numbers that don't lie.
95
+ console.log('');
96
+ console.log(` ${b(cream('Your record'))}`);
97
+ console.log(` ${cream(String(stats.total))} ${sage('routed')} ${slate('·')} ${cream(String(stats.judged))} ${sage('you\'ve judged')} ${slate('·')} ${cream(String(stats.pending))} ${sage('not yet judged')}`);
98
+ if (stats.judged > 0) {
99
+ const bits = [green(`✓ ${stats.kept} kept`)];
100
+ if (stats.reverted) bits.push(ember(`↩ ${stats.reverted} reverted`));
101
+ if (stats.superseded) bits.push(peach(`⟳ ${stats.superseded} redone`));
102
+ if (stats.failed) bits.push(ember(`✗ ${stats.failed} flopped`));
103
+ console.log(` ${bits.join(slate(' · '))}`);
65
104
  }
66
-
67
- const routes = Object.entries(stats.byRoute);
68
- if (routes.length > 0) {
69
- console.log('');
70
- console.log(` ${b(cream('By route'))}`);
71
- for (const [route, r] of routes.sort((a, b) => b[1].total - a[1].total)) {
72
- const rate = r.total > 0 ? Math.round((r.kept / r.total) * 100) : 0;
73
- console.log(` ${cream(route.padEnd(14))} ${sage(`${r.total} labeled`)} ${slate('·')} ${green(`${rate}% kept`)}`);
74
- }
105
+ if (stats.autoFailed > 0) {
106
+ console.log(` ${slate(`(${stats.autoFailed} didn't complete — route errors phewsh logged for you, not your call)`)}`);
75
107
  }
76
108
 
77
- const modes = Object.entries(stats.byMode);
78
- if (modes.length > 0) {
79
- console.log('');
80
- console.log(` ${b(cream('By mode'))}`);
81
- for (const [mode, m] of modes.sort((a, b) => b[1].total - a[1].total)) {
82
- const rate = m.total > 0 ? Math.round((m.kept / m.total) * 100) : 0;
83
- console.log(` ${cream(mode.padEnd(14))} ${sage(`${m.total} labeled`)} ${slate('·')} ${green(`${rate}% kept`)}`);
84
- }
85
- }
109
+ showPayoff(stats);
86
110
 
87
- const recent = recentDecisions(8);
88
- if (recent.length > 0) {
111
+ // The ask — focused on recent, real calls, with a dead-simple next step.
112
+ const pending = pendingDecisions({ substantive: true }).slice(-6).reverse();
113
+ if (pending.length > 0) {
89
114
  console.log('');
90
- console.log(` ${b(cream('Recent'))}`);
91
- for (const d of recent) {
92
- console.log(` ${slate(d.id.padEnd(10))} ${outcomeBadge(d)} ${slate(fmtAgo(d.ts).padStart(3))} ${sage(d.summary.slice(0, 56))}`);
93
- }
115
+ console.log(` ${b(cream('Give a verdict'))} ${slate('— your recent calls (skip the trivial ones)')}`);
116
+ pending.forEach((d, i) => {
117
+ let s = (d.summary || '').replace(/\s+/g, ' ');
118
+ if (s.length > 44) s = s.slice(0, 43).trimEnd() + '…';
119
+ const via = (continuity.labelFor(d.route) || d.route).padEnd(12);
120
+ console.log(` ${teal(String(i + 1))} ${cream(s.padEnd(45))} ${slate(via)} ${slate(fmtAgo(d.ts))}`);
121
+ });
122
+ console.log('');
123
+ console.log(` ${teal('→')} ${sage('Walk through them:')} ${cream('phewsh outcomes label')} ${slate('· 20 seconds, Enter skips')}`);
124
+ console.log(` ${slate('or in a session, just tap')} ${cream('1-4')} ${slate('right after any answer.')}`);
94
125
  }
95
126
 
96
127
  showBypasses();
97
-
98
- if (stats.pending > 0) {
99
- console.log('');
100
- console.log(` ${sage('Label pending decisions:')} ${cream('phewsh outcomes label')}`);
101
- }
102
128
  console.log('');
103
129
  }
104
130
 
105
131
  function labelInteractive() {
106
- const pending = pendingDecisions();
132
+ // Newest first, and capped — you judge best while the work is fresh, and a
133
+ // 76-item slog through last week's test prompts is exactly the chore that
134
+ // made this feel pointless. The recent calls carry the signal.
135
+ const CAP = 12;
136
+ const all = pendingDecisions({ substantive: true }).reverse(); // newest first
137
+ const pending = all.slice(0, CAP);
107
138
  if (pending.length === 0) {
108
- console.log(`\n ${teal('●')} ${sage('No pending decisionseverything is labeled.')}\n`);
139
+ console.log(`\n ${teal('●')} ${sage('Nothing substantive to judge your real calls are all labeled.')}\n`);
109
140
  return;
110
141
  }
111
142
 
112
143
  console.log('');
113
- console.log(` ${b(cream(`${pending.length} pending decision${pending.length !== 1 ? 's' : ''}`))}`);
114
- console.log(` ${slate('1 kept · 2 reverted · 3 superseded · 4 failed · enter = skip · q = quit')}`);
144
+ const more = all.length > CAP ? slate(` (most recent ${CAP}; ${all.length - CAP} older left alone)`) : '';
145
+ console.log(` ${b(cream(`Judging your recent call${pending.length !== 1 ? 's' : ''}`))}${more}`);
146
+ console.log(` ${slate('For each: what happened to it?')}`);
147
+ console.log(` ${green('1')} ${sage('kept it')} ${ember('2')} ${sage('undid it')} ${peach('3')} ${sage('redid it differently')} ${ember('4')} ${sage('it flopped')}`);
148
+ console.log(` ${slate('Enter = skip (judge later) · q = stop. This is what teaches phewsh.')}`);
115
149
  ui.divider('line');
116
150
 
117
151
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -438,13 +438,16 @@ async function main() {
438
438
  }
439
439
 
440
440
  const oStats = outcomeStats();
441
- const oLabeled = oStats.total - oStats.pending;
442
441
  if (oStats.total > 0) {
443
- const keptRate = oLabeled > 0 ? Math.round((oStats.kept / oLabeled) * 100) + '% kept' : 'unlabeled';
444
- row('RECORD', cream(`${oStats.total} decision${oStats.total !== 1 ? 's' : ''}`) + slate(' · ') + sage(keptRate)
445
- + (oStats.pending > 0 ? slate(' · ') + peach(`${oStats.pending} awaiting outcome`) + slate(' — /outcomes') : ''));
442
+ // Plain language: what phewsh has logged vs what you've taught it. The
443
+ // kept-rate counts only the calls you actually judged (auto route-errors
444
+ // don't drag it down) see /outcomes for the full picture + payoff.
445
+ const tail = oStats.judged > 0
446
+ ? slate(' · ') + sage(`${Math.round((oStats.kept / oStats.judged) * 100)}% of what you judged, you kept`)
447
+ : slate(' · ') + peach('none judged yet — tell it what you kept');
448
+ row('RECORD', cream(`${oStats.total} routed`) + tail + slate(' — /outcomes'));
446
449
  } else {
447
- row('RECORD', slate('empty — decisions and outcomes accumulate as you work'));
450
+ row('RECORD', slate('empty — phewsh starts logging what you route as you work'));
448
451
  }
449
452
  } catch (cockpitErr) {
450
453
  console.log(` ${slate('(cockpit row unavailable — ' + cockpitErr.message + ' · PHEWSH_DEBUG=1 phewsh for details)')}`);
@@ -489,7 +492,8 @@ async function main() {
489
492
  } catch { /* no settings = ambient off */ }
490
493
 
491
494
  let pending = 0;
492
- try { pending = pendingDecisions({ project: projectName }).length; } catch { /* best-effort */ }
495
+ // Only substantive calls never nag the user to "judge" a greeting.
496
+ try { pending = pendingDecisions({ project: projectName, substantive: true }).length; } catch { /* best-effort */ }
493
497
 
494
498
  let installed = [];
495
499
  try { installed = listHarnesses().filter(h => h.installed).map(h => h.id); } catch { /* best-effort */ }
@@ -644,7 +648,7 @@ async function main() {
644
648
  });
645
649
  lastTurnFailure = null;
646
650
  awaitingOutcome = decisionId;
647
- console.log(slate(` via ${HARNESSES[harnessId].label} · outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing`));
651
+ console.log(slate(` via ${HARNESSES[harnessId].label} · how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing`));
648
652
  return true;
649
653
  } catch (err) {
650
654
  turnInFlight = false;
@@ -653,7 +657,7 @@ async function main() {
653
657
  console.log(`\n ${slate('cancelled — esc')}`);
654
658
  return true; // user's call, not a failure: no fallback offer
655
659
  }
656
- try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
660
+ try { labelOutcome(decisionId, 'failed', null, { auto: true }); } catch { /* keep going */ }
657
661
  recordSessionEvent(harnessId, projectName, 'task_complete', {
658
662
  taskId: decisionId, success: false, summary: input.slice(0, 140),
659
663
  });
@@ -684,7 +688,7 @@ async function main() {
684
688
  if (result.promptTokens) totalPromptTokens += result.promptTokens;
685
689
  if (result.completionTokens) totalCompletionTokens += result.completionTokens;
686
690
  if (result.promptTokens || result.completionTokens) {
687
- console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${modelName(currentModel)} · outcome? 1-4 or keep typing`));
691
+ console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${modelName(currentModel)} · how'd it go? 1-4 or keep typing`));
688
692
  }
689
693
  awaitingOutcome = decisionId;
690
694
  trackSap({
@@ -705,7 +709,7 @@ async function main() {
705
709
  console.log(`\n ${slate('cancelled — esc')}`);
706
710
  return true; // user's call, not a failure: no fallback offer
707
711
  }
708
- try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
712
+ try { labelOutcome(decisionId, 'failed', null, { auto: true }); } catch { /* keep going */ }
709
713
  messages.pop();
710
714
  console.error(`\n ${ember('!')} ${sage('API route failed')}${slate(' — ' + err.message.split('\n')[0])}`);
711
715
  return false;
@@ -1391,7 +1395,7 @@ async function main() {
1391
1395
  console.log(` ${teal('/harnesses')} ${sage('Agent CLIs detected on this machine')}`);
1392
1396
  console.log(` ${teal('/provider')} ${sage('Current route + what\'s available')}`);
1393
1397
  console.log(` ${teal('/fallback')} ${sage('What happens at a usage wall: ask or auto-switch')}`);
1394
- console.log(` ${teal('/outcomes')} ${sage('Decision recordkept/reverted/superseded/failed')}`);
1398
+ console.log(` ${teal('/outcomes')} ${sage('What workedtell phewsh, and it learns which tool to trust')}`);
1395
1399
  console.log('');
1396
1400
  console.log(` ${cream('session')}`);
1397
1401
  console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('Hand off to interactive Claude Code/Codex — outcome on return')}`);
@@ -2020,9 +2024,9 @@ async function main() {
2020
2024
  messages.push({ role: 'user', content: cmdArg });
2021
2025
  messages.push({ role: 'assistant', content: `[council of ${answers.length}]\n\n${answers.join('\n\n')}` });
2022
2026
  awaitingOutcome = decisionId;
2023
- console.log(slate(` council of ${answers.length} · outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing`));
2027
+ console.log(slate(` council of ${answers.length} · how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing`));
2024
2028
  } else {
2025
- try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
2029
+ try { labelOutcome(decisionId, 'failed', null, { auto: true }); } catch { /* keep going */ }
2026
2030
  console.log(` ${sage('Every council member failed — check')} ${cream('/provider')}`);
2027
2031
  }
2028
2032
  console.log('');
@@ -2267,7 +2271,7 @@ async function main() {
2267
2271
  });
2268
2272
  awaitingOutcome = decisionId;
2269
2273
  console.log('');
2270
- console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing')}`);
2274
+ console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate("how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing")}`);
2271
2275
  console.log('');
2272
2276
  rl.prompt();
2273
2277
  return;
package/lib/outcomes.js CHANGED
@@ -50,8 +50,18 @@ function recordDecision({ project, route, mode, summary }) {
50
50
  return id;
51
51
  }
52
52
 
53
- /** Label a decision by id (or unambiguous id prefix). Returns the decision or null. */
54
- function labelOutcome(idOrPrefix, outcome, why = null) {
53
+ /**
54
+ * Label a decision by id (or unambiguous id prefix). Returns the decision or null.
55
+ * @param {string} idOrPrefix
56
+ * @param {string} outcome one of OUTCOMES
57
+ * @param {string|null} why one-line reason (gold for reverted/failed)
58
+ * @param {object} [opts]
59
+ * @param {boolean} [opts.auto] the SYSTEM set this (e.g. a route errored), not
60
+ * the user's judgment. Kept separate so auto-failures never masquerade as
61
+ * "you tried it and reverted it" — that distinction is what made the old
62
+ * `/outcomes` read like phewsh was broken.
63
+ */
64
+ function labelOutcome(idOrPrefix, outcome, why = null, opts = {}) {
55
65
  if (!OUTCOMES.includes(outcome)) {
56
66
  throw new Error(`Outcome must be one of: ${OUTCOMES.join(', ')}`);
57
67
  }
@@ -60,6 +70,8 @@ function labelOutcome(idOrPrefix, outcome, why = null) {
60
70
  if (matches.length !== 1) return null;
61
71
  matches[0].outcome = outcome;
62
72
  matches[0].labeledAt = new Date().toISOString();
73
+ if (opts.auto) matches[0].auto = true;
74
+ else delete matches[0].auto; // a human judgment overrides a prior auto-label
63
75
  // One-line reason — the most valuable thing about a reverted/failed call.
64
76
  // Feeds /recall ("you reverted this before — because …") and /learn.
65
77
  if (why && String(why).trim()) matches[0].why = String(why).trim().slice(0, 200);
@@ -67,10 +79,24 @@ function labelOutcome(idOrPrefix, outcome, why = null) {
67
79
  return matches[0];
68
80
  }
69
81
 
70
- /** Unlabeled decisions, oldest first. */
71
- function pendingDecisions({ project = null } = {}) {
82
+ // A routed line that isn't really a decision worth judging — greetings, one
83
+ // word fillers, or a command echo. Used to keep the "judge these" ask (and the
84
+ // nudge to label) focused on real work, not "hi". Conservative on purpose: only
85
+ // obvious noise, so genuine short prompts ("fix the bug") still count.
86
+ const TRIVIAL_RE = /^(hi|hey|hello|yo|sup|ok|okay|k|thanks|thx|ty|test|testing|cool|nice|great|good|yes|no|y|n|hi there)\b[!.\s]*$/i;
87
+ function looksTrivial(summary) {
88
+ const s = (summary || '').trim();
89
+ if (s.length < 3) return true;
90
+ if (TRIVIAL_RE.test(s)) return true;
91
+ if (/^(phewsh|\/)/.test(s) && s.split(/\s+/).length <= 3) return true; // command echo
92
+ return false;
93
+ }
94
+
95
+ /** Unlabeled decisions, oldest first. `substantive` drops greetings/filler. */
96
+ function pendingDecisions({ project = null, substantive = false } = {}) {
72
97
  return load()
73
98
  .filter(d => !d.outcome && (!project || d.project === project))
99
+ .filter(d => !substantive || !looksTrivial(d.summary))
74
100
  .sort((a, b) => a.ts.localeCompare(b.ts));
75
101
  }
76
102
 
@@ -82,31 +108,73 @@ function recentDecisions(limit = 10, { project = null } = {}) {
82
108
  .slice(0, limit);
83
109
  }
84
110
 
111
+ const MIGRATE_MARKER = path.join(OUTCOMES_DIR, '.auto-migrated-v1');
112
+
113
+ /**
114
+ * One-time cleanup of records created before the `auto` flag existed. The only
115
+ * code path that ever wrote 'failed' with no reason was the turn runner
116
+ * reacting to a route error — a genuine human "this failed" verdict is always
117
+ * offered a one-line why. So legacy failed-without-why = a route error; tag it
118
+ * so it stops masquerading as the user's judgment. Idempotent (marker-guarded).
119
+ */
120
+ function migrateLegacyAutoFailures() {
121
+ try {
122
+ if (fs.existsSync(MIGRATE_MARKER)) return;
123
+ const decisions = load();
124
+ let changed = false;
125
+ for (const d of decisions) {
126
+ if (d.outcome === 'failed' && !d.auto && !d.why) { d.auto = true; changed = true; }
127
+ }
128
+ if (changed) save(decisions);
129
+ fs.mkdirSync(OUTCOMES_DIR, { recursive: true });
130
+ fs.writeFileSync(MIGRATE_MARKER, new Date().toISOString());
131
+ } catch { /* migration is best-effort — never block the record */ }
132
+ }
133
+
134
+ /** Was this outcome set by the SYSTEM (a route errored) rather than the human? */
135
+ function isAutoLabel(d) {
136
+ return d.auto === true;
137
+ }
138
+
85
139
  /**
86
140
  * The Day-14 view: totals, per-route reliability, per-mode patterns.
87
141
  * Only labeled decisions count toward outcome rates — pending is honest noise.
88
142
  */
89
143
  function outcomeStats({ project = null } = {}) {
144
+ migrateLegacyAutoFailures();
90
145
  const all = load().filter(d => !project || d.project === project);
91
146
  const labeled = all.filter(d => d.outcome);
147
+ // A route that errored is auto-labeled 'failed' — that's a system event, not
148
+ // the user judging the work. Split it out so the headline reflects the user's
149
+ // actual calls (kept/reverted/superseded + failures they themselves marked).
150
+ const judged = labeled.filter(d => !isAutoLabel(d));
151
+ const autoFailed = labeled.filter(isAutoLabel).length;
92
152
 
93
153
  const stats = {
94
154
  total: all.length,
95
155
  pending: all.length - labeled.length,
156
+ judged: judged.length, // decisions the human actually labeled
157
+ autoFailed, // route errors phewsh recorded for you
96
158
  kept: 0, reverted: 0, superseded: 0, failed: 0,
97
159
  byRoute: {},
98
160
  byMode: {},
99
161
  };
100
162
 
101
- for (const d of labeled) {
163
+ // Outcome rates count the human's judgments only — auto route-errors don't
164
+ // tank a tool's kept-rate (they're tracked separately in byRoute.errored).
165
+ for (const d of judged) {
102
166
  stats[d.outcome]++;
103
- const r = (stats.byRoute[d.route] ||= { total: 0, kept: 0, reverted: 0, superseded: 0, failed: 0 });
167
+ const r = (stats.byRoute[d.route] ||= { total: 0, kept: 0, reverted: 0, superseded: 0, failed: 0, errored: 0 });
104
168
  r.total++; r[d.outcome]++;
105
169
  if (d.mode) {
106
- const m = (stats.byMode[d.mode] ||= { total: 0, kept: 0, reverted: 0, superseded: 0, failed: 0 });
170
+ const m = (stats.byMode[d.mode] ||= { total: 0, kept: 0, reverted: 0, superseded: 0, failed: 0, errored: 0 });
107
171
  m.total++; m[d.outcome]++;
108
172
  }
109
173
  }
174
+ for (const d of labeled.filter(isAutoLabel)) {
175
+ const r = (stats.byRoute[d.route] ||= { total: 0, kept: 0, reverted: 0, superseded: 0, failed: 0, errored: 0 });
176
+ r.errored = (r.errored || 0) + 1;
177
+ }
110
178
 
111
179
  return stats;
112
180
  }
@@ -165,5 +233,5 @@ function bypassStats() {
165
233
  module.exports = {
166
234
  OUTCOMES, DECISIONS_FILE, BYPASS_REASONS, BYPASSES_FILE,
167
235
  recordDecision, labelOutcome, pendingDecisions, recentDecisions, outcomeStats,
168
- recordBypass, bypassStats,
236
+ isAutoLabel, looksTrivial, recordBypass, bypassStats,
169
237
  };
package/lib/suggest.js CHANGED
@@ -56,12 +56,14 @@ function suggestAll(s = {}) {
56
56
  });
57
57
  }
58
58
 
59
- // 2. Decisions piling up unlabeledthe record only gets smarter if labeled.
59
+ // 2. Some real calls judged, others not a verdict or two is what teaches
60
+ // phewsh which tool to trust. Payoff-framed, no scary backlog number (the
61
+ // big count read as a chore, not a payoff).
60
62
  if (pendingOutcomes >= 3) {
61
63
  out.push({
62
64
  id: 'label-outcomes',
63
65
  priority: 90,
64
- message: `${pendingOutcomes} decisions unlabeledphewsh can't learn what you keep until you say.`,
66
+ message: `Tell phewsh what worked a quick verdict teaches it which tool to trust for you.`,
65
67
  command: '/outcomes',
66
68
  why: 'Labeling kept/reverted is the dataset; it weights future routing and recall.',
67
69
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.28",
3
+ "version": "0.15.29",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"