phewsh 0.15.47 → 0.15.49

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/bin/phewsh.js CHANGED
@@ -88,6 +88,10 @@ const COMMANDS = {
88
88
  ambient: () => require('../commands/ambient')(),
89
89
  shim: () => require('../commands/shim')(),
90
90
  status: () => require('../commands/status')(),
91
+ next: () => require('../commands/next')(),
92
+ work: () => require('../commands/work')(),
93
+ remember: () => require('../commands/remember')(),
94
+ pack: () => require('../commands/pack')(),
91
95
  hook: () => require('../commands/hook')(),
92
96
  welcome: () => require('../commands/welcome')(),
93
97
  intro: () => require('../commands/welcome')(),
@@ -103,7 +107,14 @@ function showHelp() {
103
107
  const pkg = require('../package.json');
104
108
  showBrand();
105
109
  console.log(` ${g('v' + pkg.version)} · ${g('phewsh.com/cli')}\n`);
106
- console.log(` ${g('The loop: define .intent/ sync every AI tool reads → work → repeat')}`);
110
+ console.log(` ${g('Phewsh keeps work from getting lost for you, and every AI tool you use.')}`);
111
+ console.log('');
112
+ console.log(` ${b(w('the whole idea — four plain words'))}`);
113
+ console.log(` ${cyan('intent')} ${g('PROJECT what you\'re building & why')}`);
114
+ console.log(` ${cyan('next')} ${g('NEXT what should happen next — a zero-AI list')}`);
115
+ console.log(` ${cyan('work')} ${g('WORK what\'s happening now — at a glance')}`);
116
+ console.log(` ${cyan('remember')} ${g('RECORD jot a decision so it sticks & travels')}`);
117
+ console.log(` ${cyan('status')} ${g('see all four at a glance')}`);
107
118
  console.log('');
108
119
  console.log(` ${b(w('get started'))}`);
109
120
  console.log(` ${cyan('phewsh')} ${g('Open a session — routes through your installed agents')}`);
@@ -115,6 +126,7 @@ function showHelp() {
115
126
  console.log(` ${cyan('gate')} ${g('Set constraints (budget, time, skill, urgency)')}`);
116
127
  console.log(` ${cyan('context')} ${g('Export .intent/ for any AI tool')}`);
117
128
  console.log(` ${cyan('status')} ${g('git status for AI continuity — truth, record, drift, what\'s wired')}`);
129
+ console.log(` ${cyan('next')} ${g('What should happen next — a zero-AI list every tool reads')}`);
118
130
  console.log(` ${cyan('truth')} ${g('Read-only audit: versions, Git, intent, projections, conflicts')}`);
119
131
  console.log(` ${cyan('brief')} ${g('Provider-ready briefing built from verified project truth')}`);
120
132
  console.log(` ${cyan('ai')} ${g('One-shot prompt with .intent/ context')}
@@ -128,7 +140,9 @@ function showHelp() {
128
140
  console.log(` ${cyan('mcp')} ${g('Connect AI agents via MCP protocol')}`);
129
141
  console.log(` ${cyan('ambient')} ${g('Continuity without launching phewsh — enhance your other tools')}`);
130
142
  console.log(` ${cyan('shim')} ${g('Guaranteed launch banner — phewsh prints status before each tool')}`);
143
+ console.log(` ${cyan('pack')} ${g('Opt-in workflow packs (Karpathy guidelines, GSD…) — attributed, reversible')}`);
131
144
  console.log(` ${cyan('receipts')} ${g('Proof trail — what agents actually did, with evidence')}`);
145
+ console.log(` ${cyan('remember')} ${g('Jot a decision to .intent/decisions.md — every tool inherits it')}`);
132
146
  console.log(` ${cyan('outcomes')} ${g('Decision record — what was kept, reverted, or failed')}`);
133
147
  console.log(` ${cyan('bypass')} ${g('Went around phewsh? Record why — 10 seconds, no guilt')}`);
134
148
  console.log('');
@@ -0,0 +1,131 @@
1
+ // phewsh next — one of the four plain words (Project · Next · Work · Record).
2
+ // A zero-AI "what should happen next?" list that lives in `.intent/next.json`,
3
+ // so it travels with the repo and every AI tool reads the same list. Works
4
+ // for a human alone with no model in the loop — a thinking tool first.
5
+ //
6
+ // phewsh next show NOW / NEXT / DONE + phewsh's suggestion
7
+ // phewsh next add "<title>" queue something
8
+ // phewsh next start <n|id> mark it in progress (NOW)
9
+ // phewsh next done <n|id> mark it shipped (DONE)
10
+ // phewsh next drop <n|id> remove it
11
+ // phewsh next clear clear the DONE pile
12
+
13
+ const next = require('../lib/next');
14
+
15
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
16
+ const teal = (s) => `\x1b[38;5;79m${s}\x1b[0m`;
17
+ const sage = (s) => `\x1b[38;5;151m${s}\x1b[0m`;
18
+ const slate = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
19
+ const cream = (s) => `\x1b[38;5;230m${s}\x1b[0m`;
20
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
21
+
22
+ const MARK = { now: green('◐'), next: slate('○'), done: slate('✓') };
23
+ const LABEL = { now: cream('NOW '), next: teal('NEXT'), done: slate('DONE') };
24
+
25
+ function render(cwd = process.cwd()) {
26
+ const data = next.load(cwd);
27
+ const list = next.ordered(data);
28
+ console.log('');
29
+ console.log(` ${b(cream('NEXT'))} ${slate('— what should happen next ·')} ${slate('.intent/next.json')}`);
30
+ console.log('');
31
+ if (list.length === 0) {
32
+ console.log(` ${slate('Nothing queued yet.')}`);
33
+ console.log(` ${slate('Add one:')} ${cream('phewsh next add "the thing you want done"')}`);
34
+ } else {
35
+ list.forEach((it, i) => {
36
+ const n = slate(String(i + 1).padStart(2));
37
+ const title = it.state === 'done' ? slate(it.title) : cream(it.title);
38
+ console.log(` ${n} ${MARK[it.state]} ${LABEL[it.state]} ${title}`);
39
+ });
40
+ console.log('');
41
+ console.log(` ${slate('start # · done # · drop # · add "…" · clear')}`);
42
+ }
43
+
44
+ // Unify with phewsh's own recommendation: the user's list is what THEY want
45
+ // next; this one line is what phewsh notices is worth doing. Best-effort.
46
+ try {
47
+ const { suggest } = require('../lib/suggest');
48
+ const tip = suggest(buildState(cwd));
49
+ if (tip) {
50
+ console.log('');
51
+ console.log(` ${teal('⤷ phewsh suggests')} ${cream(tip.command.trim())} ${sage(tip.message)}`);
52
+ }
53
+ } catch { /* suggestion is a nicety, never required */ }
54
+ console.log('');
55
+ }
56
+
57
+ // Minimal state for the suggestion engine — mirrors session.buildSuggestState
58
+ // but standalone (no live session). Everything fail-soft.
59
+ function buildState(cwd) {
60
+ const fs = require('fs');
61
+ const path = require('path');
62
+ const hasIntentDir = fs.existsSync(path.join(cwd, '.intent'));
63
+ let intentFileCount = 0;
64
+ try {
65
+ intentFileCount = fs.readdirSync(path.join(cwd, '.intent')).filter(f => f.endsWith('.md')).length;
66
+ } catch { /* none */ }
67
+ let packsInstalled = false;
68
+ try {
69
+ const { PACKS, isInstalled } = require('../lib/packs');
70
+ packsInstalled = Object.keys(PACKS).some(name => isInstalled(name, cwd));
71
+ } catch { /* ignore */ }
72
+ let installedHarnesses = [];
73
+ try { installedHarnesses = require('../lib/harnesses').listHarnesses().filter(h => h.installed).map(h => h.id); } catch { /* ignore */ }
74
+ return { hasIntentDir, intentFileCount, installedHarnesses, packsInstalled, turnsThisSession: 1 };
75
+ }
76
+
77
+ function notFound(ref) {
78
+ console.log('');
79
+ console.log(` ${slate('No item')} ${cream(ref)}${slate('. Run')} ${cream('phewsh next')} ${slate('to see the numbered list.')}`);
80
+ console.log('');
81
+ }
82
+
83
+ function main() {
84
+ const sub = (process.argv[3] || '').toLowerCase();
85
+ const rest = process.argv.slice(4).join(' ').replace(/^["']|["']$/g, '');
86
+
87
+ if (!sub || sub === 'list' || sub === 'ls') {
88
+ render();
89
+ return;
90
+ }
91
+
92
+ if (sub === 'add' || sub === 'a') {
93
+ const item = next.add(rest);
94
+ if (!item) {
95
+ console.log(`\n ${slate('Add what? e.g.')} ${cream('phewsh next add "wire up the MCP read connector"')}\n`);
96
+ return;
97
+ }
98
+ render();
99
+ return;
100
+ }
101
+
102
+ if (sub === 'start' || sub === 'now' || sub === 'done' || sub === 'drop' || sub === 'rm') {
103
+ const ref = process.argv[4];
104
+ if (!ref) { console.log(`\n ${slate('Which one? e.g.')} ${cream('phewsh next ' + sub + ' 1')}\n`); return; }
105
+ if (sub === 'drop' || sub === 'rm') {
106
+ const removed = next.remove(ref);
107
+ if (!removed) return notFound(ref);
108
+ } else {
109
+ const state = sub === 'start' || sub === 'now' ? 'now' : 'done';
110
+ const item = next.setState(ref, state);
111
+ if (!item) return notFound(ref);
112
+ }
113
+ render();
114
+ return;
115
+ }
116
+
117
+ if (sub === 'clear') {
118
+ const data = next.load();
119
+ data.items = data.items.filter(it => it.state !== 'done');
120
+ next.save(data);
121
+ render();
122
+ return;
123
+ }
124
+
125
+ // Unknown sub → treat the whole thing as a quick add ("phewsh next fix the bug")
126
+ const item = next.add([sub, rest].filter(Boolean).join(' '));
127
+ if (item) { render(); return; }
128
+ render();
129
+ }
130
+
131
+ module.exports = main;
@@ -0,0 +1,97 @@
1
+ // phewsh pack — opt-in gateway to AI-workflow enhancements (Karpathy-style
2
+ // guidelines, GSD, …). Attributed, previewed, confirmed, reversible. phewsh's
3
+ // core stays continuity; packs are an optional layer you choose.
4
+ //
5
+ // phewsh pack list packs (available + installed)
6
+ // phewsh pack install <name> show source/license + diff, confirm, install
7
+ // phewsh pack remove <name> remove a vendored pack's marked block
8
+
9
+ const readline = require('readline');
10
+ const packs = require('../lib/packs');
11
+
12
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
13
+ const teal = (s) => `\x1b[38;5;79m${s}\x1b[0m`;
14
+ const sage = (s) => `\x1b[38;5;151m${s}\x1b[0m`;
15
+ const slate = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
16
+ const cream = (s) => `\x1b[38;5;230m${s}\x1b[0m`;
17
+ const peach = (s) => `\x1b[38;5;216m${s}\x1b[0m`;
18
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
19
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
20
+
21
+ async function confirm(q) {
22
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
23
+ return new Promise(res => rl.question(q, a => { rl.close(); res(/^y(es)?$/i.test(a.trim())); }));
24
+ }
25
+
26
+ function list() {
27
+ console.log('');
28
+ console.log(` ${b(cream('phewsh packs'))} ${sage('— optional, attributed, reversible enhancements')}`);
29
+ console.log(` ${slate('phewsh\'s core is continuity; these are extras you opt into.')}`);
30
+ console.log('');
31
+ for (const [name, p] of Object.entries(packs.PACKS)) {
32
+ const on = packs.isInstalled(name);
33
+ const tag = p.kind === 'linked' ? slate('[linked]') : (on ? green('[installed]') : slate('[available]'));
34
+ console.log(` ${cream(name.padEnd(16))} ${tag}`);
35
+ console.log(` ${slate(p.desc)}`);
36
+ console.log(` ${slate('source: ' + p.source)}`);
37
+ }
38
+ console.log('');
39
+ console.log(` ${sage('Install:')} ${cream('phewsh pack install <name>')} ${sage('Remove:')} ${cream('phewsh pack remove <name>')}`);
40
+ console.log('');
41
+ }
42
+
43
+ async function install(name) {
44
+ const p = packs.PACKS[name];
45
+ if (!p) { console.log(`\n ${yellow('Unknown pack:')} ${name}. See ${cream('phewsh pack')}.\n`); return; }
46
+
47
+ console.log('');
48
+ console.log(` ${b(cream(p.title))} ${slate('(pack: ' + name + ')')}`);
49
+ console.log(` ${slate(p.desc)}`);
50
+ console.log(` ${peach('source:')} ${slate(p.source)}`);
51
+ console.log(` ${peach('license:')} ${slate(p.license)}`);
52
+ console.log('');
53
+
54
+ if (p.kind === 'linked') {
55
+ console.log(` ${sage('This is a separate tool — phewsh doesn\'t vendor it. Install it with:')}`);
56
+ console.log(` ${cream(p.install)}`);
57
+ console.log('');
58
+ return;
59
+ }
60
+
61
+ const preview = packs.previewInstall(name);
62
+ console.log(` ${sage('Writes a clearly-marked, removable block into:')} ${cream(preview.files.join(', '))}`);
63
+ console.log(` ${slate('Preview:')}`);
64
+ preview.block.split('\n').slice(0, 8).forEach(l => console.log(` ${slate('│ ' + l)}`));
65
+ console.log(` ${slate('│ … (' + preview.block.split('\n').length + ' lines total)')}`);
66
+ console.log('');
67
+
68
+ const ok = await confirm(` ${b('Install this pack here?')} ${slate('[y/N] ')}`);
69
+ if (!ok) { console.log(` ${sage('Nothing changed.')}\n`); return; }
70
+
71
+ const { written } = packs.install(name);
72
+ console.log('');
73
+ console.log(` ${green('●')} ${b('Installed.')} ${slate('→ ' + written.join(', '))}`);
74
+ console.log(` ${sage('Remove anytime:')} ${cream('phewsh pack remove ' + name)}`);
75
+ console.log('');
76
+ }
77
+
78
+ function remove(name) {
79
+ const p = packs.PACKS[name];
80
+ if (!p) { console.log(`\n ${yellow('Unknown pack:')} ${name}.\n`); return; }
81
+ if (p.kind === 'linked') { console.log(`\n ${sage(p.title + ' is a separate tool — uninstall it the way you installed it.')}\n`); return; }
82
+ const { removed } = packs.remove(name);
83
+ console.log('');
84
+ if (removed.length) console.log(` ${green('●')} ${b('Removed.')} ${slate('← ' + removed.join(', '))}`);
85
+ else console.log(` ${sage('Not installed here — nothing to remove.')}`);
86
+ console.log('');
87
+ }
88
+
89
+ async function main() {
90
+ const sub = process.argv[3];
91
+ const name = process.argv[4];
92
+ if (sub === 'install' && name) return install(name);
93
+ if (sub === 'remove' && name) return remove(name);
94
+ return list();
95
+ }
96
+
97
+ module.exports = main;
@@ -0,0 +1,45 @@
1
+ // phewsh remember — Record's zero-AI verb (Project · Next · Work · Record).
2
+ // Jot a decision or lesson so it sticks and every AI tool inherits it.
3
+ //
4
+ // phewsh remember "we're deferring the MCP connector to phase 3"
5
+ // phewsh remember show what you've remembered
6
+
7
+ const record = require('../lib/record');
8
+
9
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
10
+ const teal = (s) => `\x1b[38;5;79m${s}\x1b[0m`;
11
+ const sage = (s) => `\x1b[38;5;151m${s}\x1b[0m`;
12
+ const slate = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
13
+ const cream = (s) => `\x1b[38;5;230m${s}\x1b[0m`;
14
+
15
+ function main() {
16
+ const text = process.argv.slice(3).join(' ').replace(/^["']|["']$/g, '');
17
+
18
+ if (!text) {
19
+ const ns = record.notes();
20
+ console.log('');
21
+ if (ns.length === 0) {
22
+ console.log(` ${slate('Nothing remembered yet.')}`);
23
+ console.log(` ${slate('Jot a decision so every tool inherits it:')}`);
24
+ console.log(` ${cream('phewsh remember "we decided to keep packs opt-in"')}`);
25
+ } else {
26
+ console.log(` ${b(cream('RECORD'))} ${slate('— what you decided · .intent/decisions.md')}`);
27
+ console.log('');
28
+ ns.slice(-12).forEach(l => console.log(` ${slate(l)}`));
29
+ }
30
+ console.log('');
31
+ return;
32
+ }
33
+
34
+ const r = record.remember(text);
35
+ console.log('');
36
+ if (r) {
37
+ console.log(` ${teal('✓')} ${sage('Remembered.')} ${slate('Every tool reading .intent/ now sees it.')}`);
38
+ console.log(` ${slate('.intent/decisions.md · ' + r.date)}`);
39
+ } else {
40
+ console.log(` ${slate('Could not write .intent/decisions.md here.')}`);
41
+ }
42
+ console.log('');
43
+ }
44
+
45
+ module.exports = main;
@@ -582,6 +582,12 @@ async function main() {
582
582
  let shimOn = false;
583
583
  try { shimOn = require('../lib/shims').shimStatus().installed.length > 0; } catch { /* shims off */ }
584
584
 
585
+ let packsInstalled = false;
586
+ try {
587
+ const { PACKS, isInstalled } = require('../lib/packs');
588
+ packsInstalled = Object.keys(PACKS).some(name => isInstalled(name));
589
+ } catch { /* packs nudge is optional */ }
590
+
585
591
  let pending = 0;
586
592
  // Only substantive calls — never nag the user to "judge" a greeting.
587
593
  try { pending = pendingDecisions({ project: projectName, substantive: true }).length; } catch { /* best-effort */ }
@@ -605,6 +611,7 @@ async function main() {
605
611
  seqStale,
606
612
  ambientOn,
607
613
  shimOn,
614
+ packsInstalled,
608
615
  commitsSinceIntent: (() => { try { return selfheal.commitsSinceIntent(); } catch { return 0; } })(),
609
616
  bestKeeper,
610
617
  };
@@ -932,6 +939,7 @@ async function main() {
932
939
  'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
933
940
  'agents', 'context', 'truth', 'brief', 'wrap', 'reconcile', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
934
941
  'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
942
+ 'pack', 'packs', 'remember',
935
943
  ]);
936
944
  const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
937
945
  let turnAbort = null; // AbortController while an API turn streams
@@ -1354,11 +1362,52 @@ async function main() {
1354
1362
  // The self-aware "what should I do?" button: phewsh reads the session's
1355
1363
  // state and hands back ranked, pickable next steps — no command to recall.
1356
1364
  if (cmd === 'next' || cmd === 'recommend' || cmd === 'guide') {
1365
+ // "Next" is one of the four words: your list (.intent/next.json) +
1366
+ // phewsh's own recommendation, unified under one verb.
1367
+ const nx = require('../lib/next');
1368
+ const nparts = cmdArg.split(/\s+/);
1369
+ const nsub = (nparts[0] || '').toLowerCase();
1370
+ const TASK_SUBS = ['add', 'a', 'start', 'now', 'done', 'drop', 'rm', 'list', 'ls', 'clear'];
1371
+ const renderTasks = () => {
1372
+ const items = nx.ordered(nx.load());
1373
+ console.log('');
1374
+ console.log(` ${b(cream('NEXT'))} ${slate('— what should happen next · .intent/next.json')}`);
1375
+ if (items.length === 0) {
1376
+ console.log(` ${slate('Empty.')} ${cream('/next add "the thing you want done"')}`);
1377
+ } else {
1378
+ items.forEach((it, i) => {
1379
+ const mark = it.state === 'now' ? green('◐') : it.state === 'done' ? slate('✓') : slate('○');
1380
+ const title = it.state === 'done' ? slate(it.title) : cream(it.title);
1381
+ console.log(` ${slate(String(i + 1).padStart(2))} ${mark} ${title}`);
1382
+ });
1383
+ console.log(` ${slate('/next start # · /next done # · /next drop # · /next add "…"')}`);
1384
+ }
1385
+ console.log('');
1386
+ };
1387
+ if (TASK_SUBS.includes(nsub)) {
1388
+ if (nsub === 'add' || nsub === 'a') nx.add(cmdArg.replace(/^\S+\s*/, '').replace(/^["']|["']$/g, ''));
1389
+ else if (nsub === 'clear') { const dd = nx.load(); dd.items = dd.items.filter(i => i.state !== 'done'); nx.save(dd); }
1390
+ else if (nsub === 'list' || nsub === 'ls') { /* render only */ }
1391
+ else { const ref = nparts[1]; if (nsub === 'drop' || nsub === 'rm') nx.remove(ref); else nx.setState(ref, (nsub === 'start' || nsub === 'now') ? 'now' : 'done'); }
1392
+ renderTasks();
1393
+ rl.prompt();
1394
+ return;
1395
+ }
1396
+ const c = nx.counts();
1357
1397
  let ranked = [];
1358
1398
  try { ranked = suggestAll(buildSuggestState()); } catch { /* best-effort */ }
1359
1399
  console.log('');
1400
+ if (c.total > 0) {
1401
+ const bits = [];
1402
+ if (c.now) bits.push(green(c.now + ' in progress'));
1403
+ if (c.next) bits.push(teal(c.next + ' queued'));
1404
+ if (c.done) bits.push(slate(c.done + ' done'));
1405
+ console.log(` ${cream('your NEXT list:')} ${bits.join(slate(' · '))} ${slate('— /next list')}`);
1406
+ console.log('');
1407
+ }
1360
1408
  if (ranked.length === 0) {
1361
- console.log(` ${teal('●')} ${sage("You're aligned — nothing pressing. Keep working, or /help for everything.")}`);
1409
+ if (c.total === 0) console.log(` ${teal('●')} ${sage("You're aligned — nothing pressing. Note what's next with /next add, or keep working.")}`);
1410
+ else console.log(` ${teal('●')} ${sage("That's your list. Nothing else pressing from phewsh.")}`);
1362
1411
  console.log('');
1363
1412
  nextChoices = null;
1364
1413
  rl.prompt();
@@ -1518,6 +1567,7 @@ async function main() {
1518
1567
  console.log(` ${cream('author .intent/')}`);
1519
1568
  console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
1520
1569
  console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
1570
+ console.log(` ${teal('/remember')} ${sage('Jot a decision to .intent/decisions.md — every tool inherits it')}`);
1521
1571
  console.log(` ${teal('/truth')} ${sage('Read-only audit of versions, Git, intent, projections, and conflicts')}`);
1522
1572
  console.log(` ${teal('/brief')} ${sage('Generate the current provider-ready verified briefing')}`);
1523
1573
  console.log(` ${teal('/wrap')} ${sage('Observe changes, contradictions, unknowns, and reconciliation needs')}`);
@@ -1536,6 +1586,7 @@ async function main() {
1536
1586
  console.log(` ${teal('/pull')} ${sage('Pull from cloud (reloads context)')}`);
1537
1587
  console.log(` ${teal('/serve')} ${sage('Execution bridge for phewsh.com/intent')}`);
1538
1588
  console.log(` ${teal('/sync')} ${sage('Check sync status')}`);
1589
+ console.log(` ${teal('/pack')} ${sage('Opt-in workflow packs (Karpathy, GSD…) — attributed, reversible')}`);
1539
1590
  console.log('');
1540
1591
  console.log(` ${cream('route — where your typing goes')}`);
1541
1592
  console.log(` ${teal('/use')} ${slate('<route>')} ${sage('Switch: claude-code, codex, gemini, cursor, opencode, api')}`);
@@ -2554,6 +2605,45 @@ async function main() {
2554
2605
  return;
2555
2606
  }
2556
2607
 
2608
+ if (cmd === 'remember') {
2609
+ // Record's zero-AI verb: jot a decision/lesson to .intent/decisions.md
2610
+ // so it sticks and every tool inherits it. No model needed.
2611
+ const rec = require('../lib/record');
2612
+ const text = cmdArg.replace(/^["']|["']$/g, '').trim();
2613
+ console.log('');
2614
+ if (!text) {
2615
+ const ns = rec.notes();
2616
+ if (ns.length === 0) {
2617
+ console.log(` ${slate('Nothing remembered yet. Jot a decision:')} ${cream('/remember we decided to keep packs opt-in')}`);
2618
+ } else {
2619
+ console.log(` ${b(cream('RECORD'))} ${slate('— what you decided · .intent/decisions.md')}`);
2620
+ ns.slice(-12).forEach(l => console.log(` ${slate(l)}`));
2621
+ }
2622
+ } else {
2623
+ const r = rec.remember(text);
2624
+ if (r) console.log(` ${green('✓')} ${sage('Remembered.')} ${slate('Every tool reading .intent/ now sees it. · .intent/decisions.md')}`);
2625
+ else console.log(` ${slate('Could not write .intent/decisions.md here.')}`);
2626
+ }
2627
+ console.log('');
2628
+ rl.prompt();
2629
+ return;
2630
+ }
2631
+
2632
+ if (cmd === 'pack' || cmd === 'packs') {
2633
+ // Opt-in workflow packs (Karpathy, GSD…) — attributed, reversible,
2634
+ // project-scoped. Delegate to the real `phewsh pack` command so the
2635
+ // preview + [y/N] confirm get the live stdin (same handoff as /intent).
2636
+ pasteMode(false);
2637
+ rl.pause();
2638
+ const { spawnSync } = require('child_process');
2639
+ const passArgs = cmdArg ? cmdArg.split(/\s+/).filter(Boolean) : [];
2640
+ spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), 'pack', ...passArgs], { stdio: 'inherit' });
2641
+ rl.resume();
2642
+ pasteMode(true);
2643
+ rl.prompt();
2644
+ return;
2645
+ }
2646
+
2557
2647
  // Unknown slash command — suggest the nearest real one instead of a dead end.
2558
2648
  const guess = closest(cmd, [...KNOWN_COMMANDS]);
2559
2649
  if (guess) {
@@ -21,7 +21,6 @@ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
21
21
 
22
22
  const ok = green('✓');
23
23
  const off = slate('○');
24
- const warn = peach('⚠');
25
24
 
26
25
  function row(label, value) {
27
26
  return ` ${slate(label.padEnd(13))} ${value}`;
@@ -53,44 +52,69 @@ function main() {
53
52
 
54
53
  console.log('');
55
54
  console.log(` ${b('😮‍💨 PHEWSH STATUS')} ${slate('· ' + projectName(cwd))}`);
55
+ console.log(` ${slate('Project · Next · Work · Record — the four answers every tool inherits')}`);
56
56
  console.log('');
57
57
 
58
- // ── Continuity asset (the product) ─────────────────────────────────
58
+ // ── PROJECT what you're building & why ───────────────────────────
59
59
  if (!hasIntent) {
60
- console.log(row('Truth', `${off} ${sage('no shared project truth here yet')}`));
61
- console.log(row('', slate('create it so the next AI inherits this one\'s context:')));
60
+ console.log(row('PROJECT', `${off} ${sage('no shared project truth here yet')}`));
61
+ console.log(row('', slate('create it so you — and the next AI inherit context:')));
62
62
  console.log(row('', cream('phewsh intent --init')));
63
63
  } else {
64
64
  const files = fs.readdirSync(intentDir).filter(f => /\.(md|json)$/.test(f));
65
65
  const has = (f) => files.includes(f);
66
- console.log(row('Truth', `${ok} ${sage('.intent present')} ${slate('(' + files.length + ' files)')}`));
67
- console.log(row('Vision', has('vision.md') ? `${ok} ${slate('loaded')}` : `${off} ${slate('missing')}`));
68
- console.log(row('Next step', (has('next.md') || has('status.md')) ? `${ok} ${slate('loaded')}` : `${off} ${slate('missing')}`));
69
-
70
- // Decision record
71
- let stats = { total: 0, pending: 0 };
72
- try { stats = require('../lib/outcomes').outcomeStats({ project: key }); } catch { /* none */ }
73
- const pend = stats.pending ? ` ${warn} ${peach(stats.pending + ' pending')}` : ` ${ok} ${slate('all labeled')}`;
74
- console.log(row('Record', `${cream(stats.total + ' decisions')}${stats.total ? pend : slate(' — none yet')}`));
66
+ const visionBit = has('vision.md') ? sage('vision loaded') : slate('vision missing');
67
+ console.log(row('PROJECT', `${ok} ${sage('.intent present')} ${slate('(' + files.length + ' files)')} ${slate('·')} ${visionBit}`));
68
+ }
75
69
 
76
- // Cross-tool continuity
70
+ // ── NEXT — what should happen next (works with zero AI) ────────────
71
+ try {
72
+ const nx = require('../lib/next');
73
+ const items = nx.ordered(nx.load(cwd));
74
+ if (items.length === 0) {
75
+ console.log(row('NEXT', `${off} ${slate('nothing queued — ')}${cream('phewsh next add "…"')}`));
76
+ } else {
77
+ const c = { now: 0, next: 0, done: 0 };
78
+ items.forEach(i => { c[i.state]++; });
79
+ const bits = [];
80
+ if (c.now) bits.push(green('◐ ' + c.now + ' in progress'));
81
+ if (c.next) bits.push(teal('○ ' + c.next + ' queued'));
82
+ if (c.done) bits.push(slate('✓ ' + c.done + ' done'));
83
+ console.log(row('NEXT', bits.join(slate(' · '))));
84
+ const focus = items.find(i => i.state === 'now') || items.find(i => i.state === 'next');
85
+ if (focus) console.log(row('', cream(focus.title)));
86
+ }
87
+ } catch { /* best-effort */ }
88
+
89
+ // ── WORK — what's being done right now (loops/runs land here later) ─
90
+ if (hasIntent) {
77
91
  try {
78
92
  const continuity = require('../lib/continuity');
79
93
  const decisions = loadDecisions();
80
94
  const tools = continuity.toolsInThread(decisions, { project: key });
81
95
  const line = continuity.lastLeftOff(decisions, { project: key });
82
- if (tools >= 2) console.log(row('Continuity', `${ok} ${sage(tools + ' tools, one thread')}${line && line.ts ? slate(' · last ' + continuity.agoText(line.ts)) : ''}`));
83
- else if (tools === 1) console.log(row('Continuity', `${slate('1 tool so far continuity builds as you switch tools')}`));
84
- else console.log(row('Continuity', slate('no activity recorded yet')));
96
+ const last = line && line.ts ? slate(' · last ' + continuity.agoText(line.ts)) : '';
97
+ if (tools >= 2) console.log(row('WORK', `${ok} ${sage(tools + ' tools, one thread')}${last}`));
98
+ else if (tools === 1) console.log(row('WORK', `${slate('1 tool so far — continuity builds as you switch tools')}${last}`));
99
+ else console.log(row('WORK', slate('no runs active yet')));
85
100
  } catch { /* best-effort */ }
101
+ }
86
102
 
87
- // Drift
103
+ // ── RECORD — what happened & what we learned ───────────────────────
104
+ if (hasIntent) {
105
+ let stats = { total: 0, pending: 0 };
106
+ try { stats = require('../lib/outcomes').outcomeStats({ project: key }); } catch { /* none */ }
107
+ const pend = stats.pending ? ` ${slate('·')} ${peach(stats.pending + ' pending')}` : (stats.total ? ` ${slate('·')} ${slate('all labeled')}` : '');
108
+ let noteBit = '';
109
+ try { const n = require('../lib/record').notes(cwd).length; if (n) noteBit = ` ${slate('·')} ${sage(n + ' remembered')}`; } catch { /* none */ }
110
+ let driftBit = '';
88
111
  try {
89
112
  const drift = require('../lib/selfheal').commitsSinceIntent(cwd);
90
- console.log(row('Drift', drift > 0
91
- ? `${warn} ${peach(drift + ' commit(s) since .intent updated')} ${slate('— reconcile to refresh')}`
92
- : `${ok} ${slate('0 — record matches the code')}`));
113
+ driftBit = drift > 0
114
+ ? ` ${slate('·')} ${peach('drift ' + drift + ' commit(s)')} ${slate('— reconcile')}`
115
+ : ` ${slate('·')} ${slate('no drift')}`;
93
116
  } catch { /* best-effort */ }
117
+ console.log(row('RECORD', `${cream(stats.total + ' decisions')}${stats.total ? pend : slate(' — none yet')}${noteBit}${driftBit}`));
94
118
  }
95
119
 
96
120
  // ── Delivery (implementation — how phewsh reaches your tools) ───────
@@ -0,0 +1,75 @@
1
+ // phewsh work — the read-only view of the fourth word
2
+ // (Project · Next · Work · Record). One glance: what are we building, what's
3
+ // next, what's happening now, what did we last decide, does anything need you?
4
+ //
5
+ // Read-only on purpose. It makes Work *visible*, not powerful — the loop/queue
6
+ // machinery is deliberately NOT here (see .intent/work-layer.md).
7
+
8
+ const work = require('../lib/work');
9
+
10
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
11
+ const teal = (s) => `\x1b[38;5;79m${s}\x1b[0m`;
12
+ const sage = (s) => `\x1b[38;5;151m${s}\x1b[0m`;
13
+ const slate = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
14
+ const cream = (s) => `\x1b[38;5;230m${s}\x1b[0m`;
15
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
16
+ const peach = (s) => `\x1b[38;5;216m${s}\x1b[0m`;
17
+
18
+ function trunc(s, n = 88) {
19
+ s = String(s).replace(/\s+/g, ' ').trim();
20
+ return s.length > n ? s.slice(0, n - 1) + '…' : s;
21
+ }
22
+
23
+ function main() {
24
+ const v = work.workView();
25
+ console.log('');
26
+ console.log(` ${b('😮‍💨 PHEWSH WORK')} ${slate('· ' + v.project.name)}`);
27
+ console.log('');
28
+
29
+ // Project — what we're building
30
+ console.log(` ${cream('Project')}`);
31
+ if (v.project.present) {
32
+ console.log(` ${sage(v.project.name)}${v.project.tagline ? slate(' — ' + trunc(v.project.tagline, 70)) : ''}`);
33
+ } else {
34
+ console.log(` ${slate('no .intent/ here yet — ')}${cream('phewsh intent --init')}`);
35
+ }
36
+
37
+ // Next — what should happen next
38
+ console.log(` ${cream('Next')}`);
39
+ if (v.next.now) {
40
+ console.log(` ${green('Now:')} ${sage(trunc(v.next.now))}`);
41
+ } else if (v.next.topQueued) {
42
+ console.log(` ${slate('Up next (not started):')} ${sage(trunc(v.next.topQueued))} ${slate('· phewsh next start 1')}`);
43
+ } else {
44
+ console.log(` ${slate('nothing queued — ')}${cream('phewsh next add "…"')}`);
45
+ }
46
+
47
+ // Work — what's being done now
48
+ console.log(` ${cream('Work')}`);
49
+ if (v.work.tool) {
50
+ console.log(` ${slate('Tool:')} ${sage(v.work.tool)}`);
51
+ console.log(` ${slate('Status:')} ${v.work.active ? green('active session') : slate('last ' + v.work.lastAgo)}`);
52
+ } else {
53
+ console.log(` ${slate('no active run detected')}`);
54
+ }
55
+
56
+ // Record — what we last decided / learned
57
+ console.log(` ${cream('Record')}`);
58
+ if (v.record.latest) {
59
+ console.log(` ${slate('Latest:')} ${sage(trunc(v.record.latest))}`);
60
+ } else {
61
+ console.log(` ${slate('nothing recorded yet — ')}${cream('phewsh remember "…"')}`);
62
+ }
63
+
64
+ // Review — does anything need a human?
65
+ console.log(` ${cream('Review')}`);
66
+ if (v.review.clear) {
67
+ console.log(` ${green('✓')} ${sage('Nothing needs human review right now')}`);
68
+ } else {
69
+ v.review.needs.forEach(n => console.log(` ${peach('⚠')} ${sage(n)}`));
70
+ }
71
+
72
+ console.log('');
73
+ }
74
+
75
+ module.exports = main;
package/lib/next.js ADDED
@@ -0,0 +1,111 @@
1
+ // "Next" — the structured backing for one of phewsh's four plain words
2
+ // (Project · Next · Work · Record). A dead-simple, zero-AI task list that
3
+ // answers "what should happen next?" for a human alone OR for any AI tool
4
+ // reading the project. Lives in `.intent/next.json` so it travels with the
5
+ // repo and every tool inherits the same list — no account, no model needed.
6
+ //
7
+ // Three states only, on purpose: NOW (doing it), NEXT (queued), DONE (shipped).
8
+ // CG called this a "queue"; we call it Next because everyone understands it.
9
+ // Pure + fail-soft: a corrupt/odd-shaped file degrades to empty, never throws.
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const STATES = ['now', 'next', 'done'];
15
+ // Display order — what you act on first, what's queued, what's already done.
16
+ const ORDER = { now: 0, next: 1, done: 2 };
17
+
18
+ function intentDir(cwd = process.cwd()) {
19
+ return path.join(cwd, '.intent');
20
+ }
21
+
22
+ function nextFile(cwd = process.cwd()) {
23
+ return path.join(intentDir(cwd), 'next.json');
24
+ }
25
+
26
+ function load(cwd = process.cwd()) {
27
+ try {
28
+ const data = JSON.parse(fs.readFileSync(nextFile(cwd), 'utf-8'));
29
+ const items = Array.isArray(data?.items) ? data.items.filter(it => it && it.id && it.title) : [];
30
+ return { version: 1, items };
31
+ } catch {
32
+ return { version: 1, items: [] };
33
+ }
34
+ }
35
+
36
+ function save(data, cwd = process.cwd()) {
37
+ try {
38
+ fs.mkdirSync(intentDir(cwd), { recursive: true });
39
+ fs.writeFileSync(nextFile(cwd), JSON.stringify({ version: 1, items: data.items }, null, 2) + '\n');
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ /** Items in display order: NOW, then NEXT, then DONE; stable within a state. */
47
+ function ordered(data) {
48
+ return data.items
49
+ .map((it, i) => ({ it, i }))
50
+ .sort((a, b) => (ORDER[a.it.state] ?? 1) - (ORDER[b.it.state] ?? 1) || a.i - b.i)
51
+ .map(x => x.it);
52
+ }
53
+
54
+ /** Resolve a user reference (exact id, or 1-based number in display order). */
55
+ function resolve(data, ref) {
56
+ if (ref == null) return null;
57
+ const byId = data.items.find(it => it.id === ref);
58
+ if (byId) return byId;
59
+ const n = parseInt(ref, 10);
60
+ if (Number.isInteger(n) && n >= 1) {
61
+ const list = ordered(data);
62
+ if (n <= list.length) return list[n - 1];
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function add(title, cwd = process.cwd()) {
68
+ const t = (title || '').trim();
69
+ if (!t) return null;
70
+ const data = load(cwd);
71
+ const id = 'n' + Date.now().toString(36).slice(-4) + Math.random().toString(36).slice(2, 4);
72
+ const now = new Date().toISOString();
73
+ const item = { id, title: t.slice(0, 200), state: 'next', created: now, updated: now };
74
+ data.items.push(item);
75
+ save(data, cwd);
76
+ return item;
77
+ }
78
+
79
+ function setState(ref, state, cwd = process.cwd()) {
80
+ if (!STATES.includes(state)) return null;
81
+ const data = load(cwd);
82
+ const item = resolve(data, ref);
83
+ if (!item) return null;
84
+ item.state = state;
85
+ item.updated = new Date().toISOString();
86
+ if (state === 'done') item.done = item.updated;
87
+ save(data, cwd);
88
+ return item;
89
+ }
90
+
91
+ function remove(ref, cwd = process.cwd()) {
92
+ const data = load(cwd);
93
+ const item = resolve(data, ref);
94
+ if (!item) return null;
95
+ data.items = data.items.filter(it => it.id !== item.id);
96
+ save(data, cwd);
97
+ return item;
98
+ }
99
+
100
+ /** Counts by state — for status/banner summaries. */
101
+ function counts(cwd = process.cwd()) {
102
+ const { items } = load(cwd);
103
+ return {
104
+ now: items.filter(i => i.state === 'now').length,
105
+ next: items.filter(i => i.state === 'next').length,
106
+ done: items.filter(i => i.state === 'done').length,
107
+ total: items.length,
108
+ };
109
+ }
110
+
111
+ module.exports = { STATES, load, save, ordered, resolve, add, setState, remove, counts, nextFile };
package/lib/packs.js ADDED
@@ -0,0 +1,127 @@
1
+ // phewsh packs — an opt-in gateway to proven AI-workflow enhancements.
2
+ //
3
+ // phewsh's core is project truth + continuity + record. It is NOT a grab-bag of
4
+ // every viral CLAUDE.md. But good ideas exist out there, and phewsh is a natural
5
+ // front door to them — IF they stay optional, attributed, and reversible. So:
6
+ // • Nothing is vendored or injected by default (never in ambient/first-run).
7
+ // • `phewsh pack install <name>` shows the source + license + a diff preview
8
+ // and asks before writing.
9
+ // • `phewsh pack remove <name>` cleanly reverses it (marked block only).
10
+ //
11
+ // Two kinds of pack:
12
+ // • vendored — phewsh writes a clearly-marked block into the project's agent
13
+ // files (e.g. Karpathy's coding guidelines into CLAUDE.md/AGENTS.md).
14
+ // • linked — phewsh doesn't vendor it; it points you at the upstream
15
+ // installer (e.g. GSD is its own tool — we hand you its command).
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const KARPATHY_GUIDELINES = `## Coding guidelines
21
+ **Tradeoff:** these bias toward caution over speed. For trivial tasks, use judgment.
22
+
23
+ ### 1. Think before coding
24
+ State assumptions explicitly; if uncertain, ask. If multiple interpretations exist, present them — don't pick silently. If a simpler approach exists, say so. If something's unclear, stop and name it.
25
+
26
+ ### 2. Simplicity first
27
+ Minimum code that solves the problem, nothing speculative. No features beyond what was asked, no abstractions for single-use code, no error handling for impossible cases. If 200 lines could be 50, rewrite. Ask: "would a senior engineer call this overcomplicated?"
28
+
29
+ ### 3. Surgical changes
30
+ Touch only what you must. Don't "improve" adjacent code, don't refactor what isn't broken, match existing style. Remove only the orphans YOUR change created; mention pre-existing dead code, don't delete it. Every changed line should trace to the request.
31
+
32
+ ### 4. Goal-driven execution
33
+ Turn tasks into verifiable goals ("add validation" → "write tests for invalid inputs, then make them pass"). For multi-step work, state a brief plan with a verify step each. Strong success criteria let you loop independently.
34
+
35
+ **Working if:** fewer unnecessary diffs, fewer rewrites from overcomplication, and clarifying questions come before mistakes, not after.`;
36
+
37
+ const PACKS = {
38
+ 'karpathy-style': {
39
+ kind: 'vendored',
40
+ title: 'Karpathy-style coding guidelines',
41
+ desc: 'Behavioral guidelines that reduce common LLM coding mistakes (think-before-coding, simplicity, surgical changes, goal-driven loops).',
42
+ source: "Andrej Karpathy's CLAUDE.md · github.com/multica-ai/andrej-karpathy-skills",
43
+ license: 'review the source repo for license before redistributing',
44
+ targets: ['CLAUDE.md', 'AGENTS.md'],
45
+ content: KARPATHY_GUIDELINES,
46
+ },
47
+ gsd: {
48
+ kind: 'linked',
49
+ title: 'GSD — Get Shit Done',
50
+ desc: 'A Claude-centric workflow augmentation (hooks, commands, planning). Its own tool — phewsh just points you at it; it is not vendored.',
51
+ source: 'github.com/gsd-build/get-shit-done',
52
+ license: 'see the GSD repo',
53
+ install: 'npx @gsd-build/get-shit-done init # follow the GSD repo for current instructions',
54
+ },
55
+ };
56
+
57
+ function markers(name) {
58
+ return { start: `<!-- phewsh-pack:${name} START -->`, end: `<!-- phewsh-pack:${name} END -->` };
59
+ }
60
+
61
+ function blockFor(name, pack) {
62
+ const m = markers(name);
63
+ const header = `# ${pack.title} (phewsh pack: ${name})\n`
64
+ + `> Source: ${pack.source}\n`
65
+ + `> Opt-in via \`phewsh pack install ${name}\` · remove: \`phewsh pack remove ${name}\`\n\n`;
66
+ return `${m.start}\n${header}${pack.content}\n${m.end}`;
67
+ }
68
+
69
+ function isInstalled(name, cwd = process.cwd()) {
70
+ const pack = PACKS[name];
71
+ if (!pack || pack.kind !== 'vendored') return false;
72
+ return pack.targets.some(f => {
73
+ try { return fs.readFileSync(path.join(cwd, f), 'utf-8').includes(markers(name).start); }
74
+ catch { return false; }
75
+ });
76
+ }
77
+
78
+ // Returns the files it WOULD write to + the block, without writing — for preview.
79
+ function previewInstall(name, cwd = process.cwd()) {
80
+ const pack = PACKS[name];
81
+ if (!pack || pack.kind !== 'vendored') return null;
82
+ return { files: pack.targets, block: blockFor(name, pack) };
83
+ }
84
+
85
+ function install(name, cwd = process.cwd()) {
86
+ const pack = PACKS[name];
87
+ if (!pack || pack.kind !== 'vendored') return { written: [] };
88
+ const block = blockFor(name, pack);
89
+ const m = markers(name);
90
+ const written = [];
91
+ for (const f of pack.targets) {
92
+ const fp = path.join(cwd, f);
93
+ let existing = '';
94
+ try { existing = fs.readFileSync(fp, 'utf-8'); } catch { /* new file */ }
95
+ if (existing.includes(m.start)) {
96
+ // refresh in place
97
+ const re = new RegExp(escapeRe(m.start) + '[\\s\\S]*?' + escapeRe(m.end));
98
+ fs.writeFileSync(fp, existing.replace(re, block));
99
+ } else {
100
+ fs.writeFileSync(fp, (existing ? existing.replace(/\s*$/, '') + '\n\n' : '') + block + '\n');
101
+ }
102
+ written.push(f);
103
+ }
104
+ return { written };
105
+ }
106
+
107
+ function remove(name, cwd = process.cwd()) {
108
+ const pack = PACKS[name];
109
+ if (!pack || pack.kind !== 'vendored') return { removed: [] };
110
+ const m = markers(name);
111
+ const removed = [];
112
+ for (const f of pack.targets) {
113
+ const fp = path.join(cwd, f);
114
+ try {
115
+ const existing = fs.readFileSync(fp, 'utf-8');
116
+ if (!existing.includes(m.start)) continue;
117
+ const re = new RegExp('\\n*' + escapeRe(m.start) + '[\\s\\S]*?' + escapeRe(m.end) + '\\n*', 'g');
118
+ fs.writeFileSync(fp, existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').replace(/^\n+/, ''));
119
+ removed.push(f);
120
+ } catch { /* not there */ }
121
+ }
122
+ return { removed };
123
+ }
124
+
125
+ function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
126
+
127
+ module.exports = { PACKS, install, remove, isInstalled, previewInstall, blockFor };
package/lib/record.js ADDED
@@ -0,0 +1,47 @@
1
+ // "Record" — the human-authored side of one of the four words
2
+ // (Project · Next · Work · Record). `phewsh remember "<x>"` appends a dated
3
+ // line to `.intent/decisions.md` so a decision or lesson sticks and travels
4
+ // with the repo — every AI tool reads it as project truth. Zero-AI: a person
5
+ // alone can jot WHY they did something, and the next session (human or model)
6
+ // inherits it. The auto-captured ledger (~/.phewsh/outcomes) is separate: that
7
+ // is routed actions labeled kept/reverted; this is what you chose to remember.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const LINE_RE = /^- \d{4}-\d{2}-\d{2} — /;
13
+
14
+ function recordFile(cwd = process.cwd()) {
15
+ return path.join(cwd, '.intent', 'decisions.md');
16
+ }
17
+
18
+ function remember(text, cwd = process.cwd()) {
19
+ const t = (text || '').trim();
20
+ if (!t) return null;
21
+ const fp = recordFile(cwd);
22
+ const date = new Date().toISOString().slice(0, 10);
23
+ const line = `- ${date} — ${t.replace(/\s+/g, ' ')}\n`;
24
+ try {
25
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
26
+ if (!fs.existsSync(fp)) {
27
+ fs.writeFileSync(fp, '# Decisions\n\n> What we decided and why — append-only. Captured with `phewsh remember "…"`.\n\n' + line);
28
+ } else {
29
+ const existing = fs.readFileSync(fp, 'utf-8');
30
+ fs.writeFileSync(fp, existing.replace(/\n*$/, '\n') + line);
31
+ }
32
+ return { date, text: t };
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /** The remembered lines, oldest first. */
39
+ function notes(cwd = process.cwd()) {
40
+ try {
41
+ return fs.readFileSync(recordFile(cwd), 'utf-8').split('\n').filter(l => LINE_RE.test(l));
42
+ } catch {
43
+ return [];
44
+ }
45
+ }
46
+
47
+ module.exports = { remember, notes, recordFile };
package/lib/shims.js CHANGED
@@ -127,8 +127,10 @@ function preflightBanner(bin, cwd = process.cwd()) {
127
127
  const d = statusDrift(cwd);
128
128
  if (d && d.tracked && d.commitsSince > 0) record = `${d.commitsSince} behind`;
129
129
  } catch { /* nicety */ }
130
- const continuity = decisionCount(cwd) >= 1 ? 'healthy' : 'building';
131
- return `${C.sage('😮‍💨 phewsh')} ${C.teal('')} ${C.slate('intent: ' + files + ' files · record: ' + record + ' · continuity: ' + continuity)} ${C.slate('· phewsh status ' + bin)}`;
130
+ let nextBit = '';
131
+ try { const c = require('./next').counts(cwd); if (c.now + c.next > 0) nextBit = ' · next: ' + (c.now + c.next) + ' open'; } catch { /* nicety */ }
132
+ // The four words, compact: Project · Next · Record (Work = this launch).
133
+ return `${C.sage('😮‍💨 phewsh')} ${C.teal('✓')} ${C.slate('project: ' + files + ' files' + nextBit + ' · record: ' + record)} ${C.slate('· phewsh status → ' + bin)}`;
132
134
  } catch {
133
135
  return `😮‍💨🤫 phewsh active · → ${bin}`; // never break the launch
134
136
  }
package/lib/suggest.js CHANGED
@@ -40,6 +40,7 @@ function suggestAll(s = {}) {
40
40
  ambientOn = false,
41
41
  shimOn = false,
42
42
  commitsSinceIntent = 0,
43
+ packsInstalled = false, // any opt-in workflow pack present in cwd
43
44
  bestKeeper = null, // { route, label, keptRate, total } from the record, or null
44
45
  } = s;
45
46
 
@@ -145,6 +146,19 @@ function suggestAll(s = {}) {
145
146
  });
146
147
  }
147
148
 
149
+ // 7. Workflow packs — lowest-leverage convenience, surfaced only once the
150
+ // moat work (intent, outcomes, ambient, shim) is handled. phewsh core stays
151
+ // continuity; packs are opt-in, attributed schools of thought you can add.
152
+ if (hasIntentDir && !packsInstalled && turnsThisSession >= 2) {
153
+ out.push({
154
+ id: 'try-packs',
155
+ priority: 22,
156
+ message: `One continuity layer, many schools of thought — add an opt-in workflow pack (Karpathy, GSD…).`,
157
+ command: '/pack',
158
+ why: 'Attributed, reversible, project-scoped. Browse with /pack; nothing is injected without your y/N.',
159
+ });
160
+ }
161
+
148
162
  return out.sort((a, b) => b.priority - a.priority);
149
163
  }
150
164
 
package/lib/work.js ADDED
@@ -0,0 +1,105 @@
1
+ // "Work" — the read-only view for the fourth plain word
2
+ // (Project · Next · Work · Record). This is a DERIVED aggregator, not a new
3
+ // store: it reads state that already exists and answers, in one glance, "what's
4
+ // happening right now?" No automation, no loop runner, no persistence. Work is
5
+ // made *visible* here; bounding and running it is a later, separate phase
6
+ // (see .intent/work-layer.md — the design fence).
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ function projectSummary(cwd) {
13
+ const intentDir = path.join(cwd, '.intent');
14
+ const present = fs.existsSync(intentDir);
15
+ let name = path.basename(cwd);
16
+ try {
17
+ const m = JSON.parse(fs.readFileSync(path.join(intentDir, 'project.json'), 'utf-8'));
18
+ if (m && m.name) name = m.name;
19
+ } catch { /* fall back to dir name */ }
20
+ let tagline = null;
21
+ try {
22
+ const v = fs.readFileSync(path.join(intentDir, 'vision.md'), 'utf-8');
23
+ const m = v.match(/##\s*North Star\s*\n+([^\n]+)/);
24
+ if (m) tagline = m[1].trim().replace(/\.+$/, '').slice(0, 120);
25
+ } catch { /* no tagline */ }
26
+ return { present, name, tagline };
27
+ }
28
+
29
+ function nextSummary(cwd) {
30
+ try {
31
+ const nx = require('./next');
32
+ const items = nx.ordered(nx.load(cwd));
33
+ const now = items.find(i => i.state === 'now') || null;
34
+ const queued = items.filter(i => i.state === 'next');
35
+ return { now: now ? now.title : null, queuedCount: queued.length, topQueued: queued[0] ? queued[0].title : null };
36
+ } catch { return { now: null, queuedCount: 0, topQueued: null }; }
37
+ }
38
+
39
+ function loadDecisions() {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.phewsh', 'outcomes', 'decisions.json'), 'utf-8'));
42
+ } catch { return []; }
43
+ }
44
+
45
+ function workSummary(cwd) {
46
+ // No reliable standalone "current tool" detection exists, so derive from the
47
+ // continuity thread: who acted last for this project, and how recently.
48
+ // "active" = recent enough to look like a live session.
49
+ try {
50
+ const continuity = require('./continuity');
51
+ const project = path.basename(cwd);
52
+ const last = continuity.lastLeftOff(loadDecisions(), { project });
53
+ if (!last || !last.ts) return { tool: null, active: false, lastAgo: null };
54
+ let tool = null;
55
+ try { tool = continuity.labelFor(last.route); } catch { tool = last.route || null; }
56
+ const ageMs = Date.now() - new Date(last.ts).getTime();
57
+ const active = Number.isFinite(ageMs) && ageMs >= 0 && ageMs < 15 * 60 * 1000;
58
+ return { tool, active, lastAgo: continuity.agoText(last.ts) };
59
+ } catch { return { tool: null, active: false, lastAgo: null }; }
60
+ }
61
+
62
+ function recordSummary(cwd) {
63
+ // Prefer a human-remembered decision; fall back to the latest routed decision.
64
+ try {
65
+ const ns = require('./record').notes(cwd);
66
+ if (ns.length) {
67
+ const latest = ns[ns.length - 1].replace(/^- \d{4}-\d{2}-\d{2} — /, '');
68
+ return { latest, source: 'remembered' };
69
+ }
70
+ } catch { /* none */ }
71
+ try {
72
+ const { recentDecisions } = require('./outcomes');
73
+ const r = recentDecisions(1, { project: path.basename(cwd) });
74
+ if (r.length && r[0].summary) return { latest: r[0].summary, source: 'decision' };
75
+ } catch { /* none */ }
76
+ return { latest: null, source: null };
77
+ }
78
+
79
+ function reviewSummary(cwd) {
80
+ // "Does anything appear to need a human?" — derived from existing signals only.
81
+ const needs = [];
82
+ try {
83
+ const { outcomeStats } = require('./outcomes');
84
+ const s = outcomeStats({ project: path.basename(cwd) });
85
+ if (s.pending > 0) needs.push(`${s.pending} decision${s.pending === 1 ? '' : 's'} to label — phewsh outcomes`);
86
+ } catch { /* none */ }
87
+ try {
88
+ const drift = require('./selfheal').commitsSinceIntent(cwd);
89
+ if (drift > 0) needs.push(`intent is ${drift} commit${drift === 1 ? '' : 's'} behind the code — reconcile`);
90
+ } catch { /* none */ }
91
+ return { needs, clear: needs.length === 0 };
92
+ }
93
+
94
+ /** The whole read-only view. Pure aggregation over existing state. */
95
+ function workView(cwd = process.cwd()) {
96
+ return {
97
+ project: projectSummary(cwd),
98
+ next: nextSummary(cwd),
99
+ work: workSummary(cwd),
100
+ record: recordSummary(cwd),
101
+ review: reviewSummary(cwd),
102
+ };
103
+ }
104
+
105
+ module.exports = { workView };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.47",
3
+ "version": "0.15.49",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"