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.
@@ -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
- console.log('');
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
- console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.role || h.label).padEnd(20))} ${slate(h.label)}${mode}${active}`);
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
- module.exports = { playIntro, LOGO, FACE };
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 };
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.22",
3
+ "version": "0.15.24",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"