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 +15 -1
- package/commands/next.js +131 -0
- package/commands/pack.js +97 -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/packs.js +127 -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,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('
|
|
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('');
|
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;
|
package/commands/pack.js
ADDED
|
@@ -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;
|
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/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
|
-
|
|
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 };
|