phewsh 0.15.48 → 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 +13 -1
- package/commands/next.js +131 -0
- package/commands/remember.js +45 -0
- package/commands/session.js +91 -1
- package/commands/status.js +45 -21
- package/commands/work.js +75 -0
- package/lib/next.js +111 -0
- package/lib/record.js +47 -0
- package/lib/shims.js +4 -2
- package/lib/suggest.js +14 -0
- package/lib/work.js +105 -0
- package/package.json +1 -1
package/bin/phewsh.js
CHANGED
|
@@ -88,6 +88,9 @@ 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')(),
|
|
91
94
|
pack: () => require('../commands/pack')(),
|
|
92
95
|
hook: () => require('../commands/hook')(),
|
|
93
96
|
welcome: () => require('../commands/welcome')(),
|
|
@@ -104,7 +107,14 @@ function showHelp() {
|
|
|
104
107
|
const pkg = require('../package.json');
|
|
105
108
|
showBrand();
|
|
106
109
|
console.log(` ${g('v' + pkg.version)} · ${g('phewsh.com/cli')}\n`);
|
|
107
|
-
console.log(` ${g('
|
|
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')}`);
|
|
108
118
|
console.log('');
|
|
109
119
|
console.log(` ${b(w('get started'))}`);
|
|
110
120
|
console.log(` ${cyan('phewsh')} ${g('Open a session — routes through your installed agents')}`);
|
|
@@ -116,6 +126,7 @@ function showHelp() {
|
|
|
116
126
|
console.log(` ${cyan('gate')} ${g('Set constraints (budget, time, skill, urgency)')}`);
|
|
117
127
|
console.log(` ${cyan('context')} ${g('Export .intent/ for any AI tool')}`);
|
|
118
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')}`);
|
|
119
130
|
console.log(` ${cyan('truth')} ${g('Read-only audit: versions, Git, intent, projections, conflicts')}`);
|
|
120
131
|
console.log(` ${cyan('brief')} ${g('Provider-ready briefing built from verified project truth')}`);
|
|
121
132
|
console.log(` ${cyan('ai')} ${g('One-shot prompt with .intent/ context')}
|
|
@@ -131,6 +142,7 @@ function showHelp() {
|
|
|
131
142
|
console.log(` ${cyan('shim')} ${g('Guaranteed launch banner — phewsh prints status before each tool')}`);
|
|
132
143
|
console.log(` ${cyan('pack')} ${g('Opt-in workflow packs (Karpathy guidelines, GSD…) — attributed, reversible')}`);
|
|
133
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')}`);
|
|
134
146
|
console.log(` ${cyan('outcomes')} ${g('Decision record — what was kept, reverted, or failed')}`);
|
|
135
147
|
console.log(` ${cyan('bypass')} ${g('Went around phewsh? Record why — 10 seconds, no guilt')}`);
|
|
136
148
|
console.log('');
|
package/commands/next.js
ADDED
|
@@ -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,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;
|
package/commands/session.js
CHANGED
|
@@ -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.
|
|
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) {
|
package/commands/status.js
CHANGED
|
@@ -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
|
-
// ──
|
|
58
|
+
// ── PROJECT — what you're building & why ───────────────────────────
|
|
59
59
|
if (!hasIntent) {
|
|
60
|
-
console.log(row('
|
|
61
|
-
console.log(row('', slate('create it so the next AI
|
|
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
|
-
|
|
67
|
-
console.log(row('
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
else console.log(row('
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
?
|
|
92
|
-
:
|
|
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) ───────
|
package/commands/work.js
ADDED
|
@@ -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/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
|
-
|
|
131
|
-
|
|
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 };
|