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.
- package/commands/outcomes.js +76 -42
- package/commands/session.js +18 -14
- package/lib/outcomes.js +76 -8
- package/lib/suggest.js +4 -2
- package/package.json +1 -1
package/commands/outcomes.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
ui.divider('line');
|
|
83
|
+
explainDeal();
|
|
52
84
|
|
|
53
85
|
if (stats.total === 0) {
|
|
54
|
-
console.log(
|
|
55
|
-
console.log(` ${
|
|
56
|
-
console.log(` ${slate('
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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('
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
114
|
-
console.log(` ${
|
|
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 });
|
package/commands/session.js
CHANGED
|
@@ -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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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 —
|
|
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
|
-
|
|
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} ·
|
|
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)} ·
|
|
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('
|
|
1398
|
+
console.log(` ${teal('/outcomes')} ${sage('What worked — tell 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} ·
|
|
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('
|
|
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
|
-
/**
|
|
54
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
});
|