phewsh 0.15.22 → 0.15.24
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 +52 -3
- package/lib/intro.js +29 -1
- package/lib/learning.js +62 -0
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -21,6 +21,7 @@ const { HARNESSES, listHarnesses, runViaHarness, cancelActive } = require('../li
|
|
|
21
21
|
const { recordDecision, labelOutcome, pendingDecisions, recentDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
|
|
22
22
|
const { suggest, suggestAll } = require('../lib/suggest');
|
|
23
23
|
const continuity = require('../lib/continuity');
|
|
24
|
+
const learning = require('../lib/learning');
|
|
24
25
|
const { closest } = require('../lib/closest');
|
|
25
26
|
const cmdHistory = require('../lib/history');
|
|
26
27
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
@@ -770,7 +771,7 @@ async function main() {
|
|
|
770
771
|
'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
|
|
771
772
|
'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
|
|
772
773
|
'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
773
|
-
'next', 'recommend', 'guide', 'thread', 'continuity',
|
|
774
|
+
'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
|
|
774
775
|
]);
|
|
775
776
|
const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
|
|
776
777
|
let turnAbort = null; // AbortController while an API turn streams
|
|
@@ -1086,7 +1087,7 @@ async function main() {
|
|
|
1086
1087
|
if (global._phewshChildren) {
|
|
1087
1088
|
global._phewshChildren.forEach(c => { try { c.kill(); } catch {} });
|
|
1088
1089
|
}
|
|
1089
|
-
|
|
1090
|
+
try { require('../lib/intro').farewell(); } catch { /* sign-off is a nicety */ }
|
|
1090
1091
|
console.log(` ${sage('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
|
|
1091
1092
|
if (decisionsThisSession > 0) {
|
|
1092
1093
|
const stillPending = pendingDecisions().length;
|
|
@@ -1158,6 +1159,46 @@ async function main() {
|
|
|
1158
1159
|
return;
|
|
1159
1160
|
}
|
|
1160
1161
|
|
|
1162
|
+
// ── /learn ─────────────────────────────────────────
|
|
1163
|
+
// What the record has learned — kept-rates by tool and by mode, so the
|
|
1164
|
+
// 100th decision is better-informed than the 1st. Honest: stays quiet
|
|
1165
|
+
// until there's real labeled signal.
|
|
1166
|
+
if (cmd === 'learn' || cmd === 'stats') {
|
|
1167
|
+
let stats = null;
|
|
1168
|
+
try { stats = outcomeStats({ project: projectName }); } catch { /* best-effort */ }
|
|
1169
|
+
const labeled = stats ? learning.totalLabeled(stats) : 0;
|
|
1170
|
+
console.log('');
|
|
1171
|
+
if (labeled < 5) {
|
|
1172
|
+
console.log(` ${teal('●')} ${sage(`Not enough labeled decisions yet (${labeled}).`)} ${slate('Label outcomes with /outcomes — the record gets smarter as you do.')}`);
|
|
1173
|
+
console.log('');
|
|
1174
|
+
rl.prompt();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
console.log(` ${b(cream('What your record has learned'))} ${slate(`— ${labeled} labeled decisions, ${projectName}`)}`);
|
|
1178
|
+
ui.divider('line');
|
|
1179
|
+
console.log(` ${sage('by tool')} ${slate('(kept-rate, best first)')}`);
|
|
1180
|
+
for (const r of learning.routeRates(stats, { minSample: 2 })) {
|
|
1181
|
+
const pct = Math.round(r.keptRate * 100);
|
|
1182
|
+
const bar = '█'.repeat(Math.round(r.keptRate * 10)).padEnd(10, '░');
|
|
1183
|
+
console.log(` ${continuity.labelFor(r.route).padEnd(14)} ${teal(bar)} ${cream(pct + '%')} ${slate(`(${r.kept}/${r.total} kept)`)}`);
|
|
1184
|
+
}
|
|
1185
|
+
const modes = learning.modeRates(stats, { minSample: 2 });
|
|
1186
|
+
if (modes.length) {
|
|
1187
|
+
console.log('');
|
|
1188
|
+
console.log(` ${sage('by kind of work')}`);
|
|
1189
|
+
for (const m of modes) {
|
|
1190
|
+
const pct = Math.round(m.keptRate * 100);
|
|
1191
|
+
console.log(` ${String(m.mode).padEnd(14)} ${cream(pct + '%')} ${slate(`(${m.kept}/${m.total} kept)`)}`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const best = learning.bestRoute(stats, { minSample: 3 });
|
|
1195
|
+
ui.divider('line');
|
|
1196
|
+
if (best) console.log(` ${teal('↪')} ${sage(`${continuity.labelFor(best.route)} keeps best for you (${Math.round(best.keptRate * 100)}%).`)} ${slate('/use ' + best.route + ' to lean on it.')}`);
|
|
1197
|
+
console.log('');
|
|
1198
|
+
rl.prompt();
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1161
1202
|
if (cmd === 'help' || cmd === 'h') {
|
|
1162
1203
|
const wantsAll = /^(all|more|full|everything)$/i.test(cmdArg.trim());
|
|
1163
1204
|
|
|
@@ -1187,6 +1228,7 @@ async function main() {
|
|
|
1187
1228
|
console.log(` ${cream('not sure what to do?')}`);
|
|
1188
1229
|
console.log(` ${teal('/next')} ${sage("phewsh reads your state and hands back the next step worth taking")}`);
|
|
1189
1230
|
console.log(` ${teal('/thread')} ${sage('where you left off — your work across every tool, one record')}`);
|
|
1231
|
+
console.log(` ${teal('/learn')} ${sage('what your record taught — which tool keeps best, by kind of work')}`);
|
|
1190
1232
|
console.log('');
|
|
1191
1233
|
console.log(` ${cream('author .intent/')}`);
|
|
1192
1234
|
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
@@ -1938,6 +1980,9 @@ async function main() {
|
|
|
1938
1980
|
console.log('');
|
|
1939
1981
|
console.log(` ${b(cream('Your AI tools'))} ${slate('— phewsh keeps them all, aligned. You never pick just one.')}`);
|
|
1940
1982
|
ui.divider('line');
|
|
1983
|
+
// The record feeding back: kept-rate per route, where it's earned.
|
|
1984
|
+
let hStats = null;
|
|
1985
|
+
try { hStats = outcomeStats({ project: projectName }); } catch { /* best-effort */ }
|
|
1941
1986
|
// Installed first, then the rest so the table also teaches what exists.
|
|
1942
1987
|
const sorted = [...harnesses].sort((a, b) => (b.installed - a.installed));
|
|
1943
1988
|
let lastGroup = null;
|
|
@@ -1950,9 +1995,13 @@ async function main() {
|
|
|
1950
1995
|
const active = route?.type === 'harness' && route.id === h.id ? ` ${teal('● active')}` : '';
|
|
1951
1996
|
const dot = h.installed ? green('●') : slate('○');
|
|
1952
1997
|
const mode = h.headless ? '' : slate(' · /work only');
|
|
1953
|
-
|
|
1998
|
+
const badge = hStats ? learning.keptBadge(hStats, h.id) : '';
|
|
1999
|
+
const rec = badge ? ` ${slate('· ' + badge)}` : '';
|
|
2000
|
+
console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.role || h.label).padEnd(20))} ${slate(h.label)}${mode}${rec}${active}`);
|
|
1954
2001
|
}
|
|
1955
2002
|
ui.divider('line');
|
|
2003
|
+
const learned = hStats ? learning.learningLine(hStats) : null;
|
|
2004
|
+
if (learned) console.log(` ${teal('↪')} ${sage(learned)} ${slate('— route accordingly')}`);
|
|
1956
2005
|
console.log(` ${sage('keep your tools, keep one record:')}`);
|
|
1957
2006
|
console.log(` ${teal('/use')} ${slate('<id>')} ${sage('route your typing through that tool')}`);
|
|
1958
2007
|
console.log(` ${teal('@<id>')} ${slate('<msg>')} ${sage('one message to one tool — context stays shared')}`);
|
package/lib/intro.js
CHANGED
|
@@ -28,6 +28,25 @@ const FACE = [
|
|
|
28
28
|
' ███ ███',
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
+
// The shush mark (🤫) — rasterized from assets/shh.svg, same 30×14 grid.
|
|
32
|
+
// Bookends the session: phew opens it, shh signs off.
|
|
33
|
+
const SHH = [
|
|
34
|
+
' ██ ███',
|
|
35
|
+
' ██ █',
|
|
36
|
+
' █ █',
|
|
37
|
+
' █ ███ ███ █',
|
|
38
|
+
' █ █',
|
|
39
|
+
'█ ██ █',
|
|
40
|
+
'█ █ █ █',
|
|
41
|
+
'█ █ █',
|
|
42
|
+
' █ █',
|
|
43
|
+
' █ █ █ █',
|
|
44
|
+
' █ █ ██ █ █ █',
|
|
45
|
+
' █ █ █ █',
|
|
46
|
+
' ██ █ ██',
|
|
47
|
+
' ██ █',
|
|
48
|
+
];
|
|
49
|
+
|
|
31
50
|
const LOGO = ['█▀█ █░█ █▀▀ █░█ █▀ █░█', '█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'];
|
|
32
51
|
|
|
33
52
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
@@ -80,4 +99,13 @@ async function playIntro(opts = {}) {
|
|
|
80
99
|
return { toolsFound: harnesses.length };
|
|
81
100
|
}
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
// Quiet sign-off — the shush mark, printed static (no animation; exit is instant).
|
|
103
|
+
function farewell(opts = {}) {
|
|
104
|
+
const { out = console.log } = opts;
|
|
105
|
+
const { slate } = ui;
|
|
106
|
+
out('');
|
|
107
|
+
for (const line of SHH) out(` ${slate(line)}`);
|
|
108
|
+
out('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { playIntro, farewell, LOGO, FACE, SHH };
|
package/lib/learning.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Learning loops — what gets smarter after the 100th decision.
|
|
2
|
+
//
|
|
3
|
+
// The decision record is already labeled (kept / reverted / superseded /
|
|
4
|
+
// failed) per route and per mode. This turns that into insight that feeds
|
|
5
|
+
// BACK into the next decision: which tool actually keeps best for you, and
|
|
6
|
+
// for which kind of work. Honest by construction — nothing surfaces until
|
|
7
|
+
// there's enough labeled signal, so there are no fake gauges.
|
|
8
|
+
//
|
|
9
|
+
// Pure: feed it outcomeStats(), get back rankings + one-line readouts.
|
|
10
|
+
|
|
11
|
+
const { labelFor } = require('./continuity');
|
|
12
|
+
|
|
13
|
+
function rate(r) { return r.total ? r.kept / r.total : 0; }
|
|
14
|
+
|
|
15
|
+
/** Per-route kept-rates, best first. Filters out thin samples. */
|
|
16
|
+
function routeRates(stats, { minSample = 1 } = {}) {
|
|
17
|
+
return Object.entries((stats && stats.byRoute) || {})
|
|
18
|
+
.map(([route, r]) => ({ route, total: r.total, kept: r.kept, keptRate: rate(r) }))
|
|
19
|
+
.filter((r) => r.total >= minSample)
|
|
20
|
+
.sort((a, b) => b.keptRate - a.keptRate || b.total - a.total);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Per-mode kept-rates, best first. */
|
|
24
|
+
function modeRates(stats, { minSample = 1 } = {}) {
|
|
25
|
+
return Object.entries((stats && stats.byMode) || {})
|
|
26
|
+
.map(([mode, m]) => ({ mode, total: m.total, kept: m.kept, keptRate: rate(m) }))
|
|
27
|
+
.filter((m) => m.total >= minSample)
|
|
28
|
+
.sort((a, b) => b.keptRate - a.keptRate || b.total - a.total);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function totalLabeled(stats) {
|
|
32
|
+
if (!stats) return 0;
|
|
33
|
+
return (stats.kept || 0) + (stats.reverted || 0) + (stats.superseded || 0) + (stats.failed || 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The route with the best kept-rate, given enough data, or null. */
|
|
37
|
+
function bestRoute(stats, { minSample = 3 } = {}) {
|
|
38
|
+
const rates = routeRates(stats, { minSample });
|
|
39
|
+
return rates.length ? rates[0] : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One honest line of what the record has learned, or null if too thin.
|
|
44
|
+
* "After 23 labeled: Codex 8/10 · Claude Code 5/9 kept"
|
|
45
|
+
*/
|
|
46
|
+
function learningLine(stats, { labeler = null, minLabeled = 5, top = 3 } = {}) {
|
|
47
|
+
const labeled = totalLabeled(stats);
|
|
48
|
+
if (labeled < minLabeled) return null;
|
|
49
|
+
const rates = routeRates(stats, { minSample: 2 }).slice(0, top);
|
|
50
|
+
if (!rates.length) return null;
|
|
51
|
+
const parts = rates.map((r) => `${labelFor(r.route, labeler)} ${r.kept}/${r.total}`);
|
|
52
|
+
return `After ${labeled} labeled: ${parts.join(' · ')} kept`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A short percentage badge for one route, or '' if too thin to be honest. */
|
|
56
|
+
function keptBadge(stats, route, { minSample = 2 } = {}) {
|
|
57
|
+
const r = stats && stats.byRoute && stats.byRoute[route];
|
|
58
|
+
if (!r || r.total < minSample) return '';
|
|
59
|
+
return `${r.kept}/${r.total} kept`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { routeRates, modeRates, bestRoute, learningLine, keptBadge, totalLabeled };
|