phewsh 0.12.4 → 0.13.1
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/README.md +21 -1
- package/bin/phewsh.js +7 -1
- package/commands/bypass.js +87 -0
- package/commands/outcomes.js +171 -0
- package/commands/session.js +337 -34
- package/commands/setup.js +137 -0
- package/lib/harnesses.js +4 -2
- package/lib/outcomes.js +159 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,7 +83,9 @@ These artifacts become persistent context for AI conversations, both in the shel
|
|
|
83
83
|
## All Commands
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
phewsh #
|
|
86
|
+
phewsh # The front door — routes through your installed agents
|
|
87
|
+
phewsh setup # Guided setup: pick your default route (60 seconds)
|
|
88
|
+
phewsh outcomes # Decision record — what was kept, reverted, or failed
|
|
87
89
|
phewsh serve # Live execution bridge for web app
|
|
88
90
|
phewsh clarify # AI-assisted artifact generation
|
|
89
91
|
phewsh intent --init # Create .intent/ without entering the shell
|
|
@@ -114,6 +116,24 @@ while a local bridge is running (`phewsh serve` or `phewsh mcp serve` — the
|
|
|
114
116
|
first executes tasks directly via Claude Code, the second routes them to
|
|
115
117
|
live connected agents; both record identical receipts).
|
|
116
118
|
|
|
119
|
+
## Outcomes
|
|
120
|
+
|
|
121
|
+
A receipt says *what ran*. An outcome says *what became of it*. Every routed
|
|
122
|
+
action in a session records a decision; label it when you actually know —
|
|
123
|
+
seconds later or three weeks later:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# in a session, right after a response: type 1-4
|
|
127
|
+
# 1 kept · 2 reverted · 3 superseded · 4 failed
|
|
128
|
+
|
|
129
|
+
phewsh outcomes # totals, kept-rate by route and mode, recent decisions
|
|
130
|
+
phewsh outcomes label # label anything still pending
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Over time this becomes the record no platform keeps for you: which decisions
|
|
134
|
+
held up, which model is most reliable for which kind of work, and where your
|
|
135
|
+
effort actually went.
|
|
136
|
+
|
|
117
137
|
## Sync
|
|
118
138
|
|
|
119
139
|
CLI and web ([phewsh.com/intent](https://phewsh.com/intent)) share the same cloud via Supabase.
|
package/bin/phewsh.js
CHANGED
|
@@ -64,6 +64,9 @@ const COMMANDS = {
|
|
|
64
64
|
watch: () => require('../commands/watch')(),
|
|
65
65
|
mcp: () => require('../commands/mcp')(),
|
|
66
66
|
receipts: () => require('../commands/receipts')(),
|
|
67
|
+
outcomes: () => require('../commands/outcomes')(),
|
|
68
|
+
bypass: () => require('../commands/bypass')(),
|
|
69
|
+
setup: () => require('../commands/setup')(),
|
|
67
70
|
update: () => require('../commands/update')(),
|
|
68
71
|
serve: () => require('../commands/serve')(),
|
|
69
72
|
sequence: () => require('../commands/sequence')(),
|
|
@@ -83,7 +86,8 @@ function showHelp() {
|
|
|
83
86
|
console.log(` ${g('The loop: define .intent/ → sync → every AI tool reads → work → repeat')}`);
|
|
84
87
|
console.log('');
|
|
85
88
|
console.log(` ${b(w('get started'))}`);
|
|
86
|
-
console.log(` ${cyan('phewsh')} ${g('Open a session —
|
|
89
|
+
console.log(` ${cyan('phewsh')} ${g('Open a session — routes through your installed agents')}`);
|
|
90
|
+
console.log(` ${cyan('phewsh setup')} ${g('Guided setup — pick your default route (60 seconds)')}`);
|
|
87
91
|
console.log(` ${cyan('phewsh clarify')} ${g('Turn a messy idea into .intent/ artifacts')}`);
|
|
88
92
|
console.log('');
|
|
89
93
|
console.log(` ${b(w('author .intent/'))}`);
|
|
@@ -100,6 +104,8 @@ function showHelp() {
|
|
|
100
104
|
console.log(` ${cyan('serve')} ${g('Execution bridge — run from phewsh.com/intent')}`);
|
|
101
105
|
console.log(` ${cyan('mcp')} ${g('Connect AI agents via MCP protocol')}`);
|
|
102
106
|
console.log(` ${cyan('receipts')} ${g('Proof trail — what agents actually did, with evidence')}`);
|
|
107
|
+
console.log(` ${cyan('outcomes')} ${g('Decision record — what was kept, reverted, or failed')}`);
|
|
108
|
+
console.log(` ${cyan('bypass')} ${g('Went around phewsh? Record why — 10 seconds, no guilt')}`);
|
|
103
109
|
console.log('');
|
|
104
110
|
console.log(` ${b(w('configure'))}`);
|
|
105
111
|
console.log(` ${cyan('login')} ${g('Identity + API key + cloud sync')}`);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// phewsh bypass — record the moment you reached past phewsh.
|
|
2
|
+
//
|
|
3
|
+
// Every bypass is the most valuable data in the dogfood experiment: it
|
|
4
|
+
// directly identifies why the front door fails. Make it 10 seconds, no guilt.
|
|
5
|
+
//
|
|
6
|
+
// phewsh bypass Quick picker
|
|
7
|
+
// phewsh bypass 2 By number (1-7)
|
|
8
|
+
// phewsh bypass faster By name
|
|
9
|
+
// phewsh bypass 7 "was on my phone" Reason + note
|
|
10
|
+
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
const ui = require('../lib/ui');
|
|
13
|
+
const { BYPASS_REASONS, recordBypass, bypassStats } = require('../lib/outcomes');
|
|
14
|
+
|
|
15
|
+
const { b, teal, sage, slate, cream, ember } = ui;
|
|
16
|
+
|
|
17
|
+
const LABELS = {
|
|
18
|
+
'forgot': 'Forgot phewsh existed in the moment',
|
|
19
|
+
'faster': 'Direct was faster',
|
|
20
|
+
'needed-editing': 'Needed interactive file editing',
|
|
21
|
+
'needed-context': 'Needed that tool\'s own context/memory',
|
|
22
|
+
'model-quality': 'Needed that model\'s quality',
|
|
23
|
+
'phewsh-in-the-way': 'phewsh got in the way',
|
|
24
|
+
'other': 'Something else',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function resolveReason(arg) {
|
|
28
|
+
if (!arg) return null;
|
|
29
|
+
const n = parseInt(arg, 10);
|
|
30
|
+
if (n >= 1 && n <= BYPASS_REASONS.length) return BYPASS_REASONS[n - 1];
|
|
31
|
+
const match = BYPASS_REASONS.find(r => r === arg || r.startsWith(arg));
|
|
32
|
+
return match || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function confirm(reason, count) {
|
|
36
|
+
console.log(`\n ${teal('●')} ${sage('Bypass recorded:')} ${cream(LABELS[reason])}`);
|
|
37
|
+
console.log(` ${slate(`${count} total — no guilt, this is the experiment working. phewsh outcomes shows the pattern.`)}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = function bypass() {
|
|
41
|
+
const args = process.argv.slice(3);
|
|
42
|
+
|
|
43
|
+
if (args[0] === 'stats') {
|
|
44
|
+
const stats = bypassStats();
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(` ${b(cream('Bypasses'))} ${slate('— why the front door got skipped')}`);
|
|
47
|
+
ui.divider('line');
|
|
48
|
+
if (stats.total === 0) {
|
|
49
|
+
console.log(` ${sage('None recorded. When you catch yourself in Claude Code directly:')} ${cream('phewsh bypass')}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(` ${cream(String(stats.total))} ${sage('total')}`);
|
|
52
|
+
for (const [reason, count] of Object.entries(stats.byReason).sort((a, b) => b[1] - a[1])) {
|
|
53
|
+
console.log(` ${cream(String(count).padStart(3))} ${sage(LABELS[reason])}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
console.log('');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reason = resolveReason(args[0]);
|
|
61
|
+
if (reason) {
|
|
62
|
+
const note = args.slice(1).join(' ');
|
|
63
|
+
confirm(reason, recordBypass(reason, note));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (args[0]) {
|
|
67
|
+
console.log(`\n ${ember('!')} ${sage('Unknown reason. Pick 1-7 or:')} ${cream(BYPASS_REASONS.join(', '))}\n`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(` ${b(cream('You opened something directly instead of phewsh — why?'))}`);
|
|
73
|
+
BYPASS_REASONS.forEach((r, i) => {
|
|
74
|
+
console.log(` ${teal(String(i + 1))} ${sage(LABELS[r])}`);
|
|
75
|
+
});
|
|
76
|
+
console.log('');
|
|
77
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
78
|
+
rl.question(` ${teal('>')} ${slate('1-7, enter = cancel: ')}`, (answer) => {
|
|
79
|
+
rl.close();
|
|
80
|
+
const picked = resolveReason(answer.trim());
|
|
81
|
+
if (!picked) {
|
|
82
|
+
console.log(` ${slate('Cancelled.')}\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
confirm(picked, recordBypass(picked));
|
|
86
|
+
});
|
|
87
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// phewsh outcomes — the accumulated record of decisions and what became of them.
|
|
2
|
+
//
|
|
3
|
+
// phewsh outcomes Stats + recent decisions
|
|
4
|
+
// phewsh outcomes label Interactively label pending decisions
|
|
5
|
+
// phewsh outcomes label <id> <o> Label one decision directly
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const ui = require('../lib/ui');
|
|
9
|
+
const {
|
|
10
|
+
OUTCOMES, recordDecision, labelOutcome,
|
|
11
|
+
pendingDecisions, recentDecisions, outcomeStats, bypassStats,
|
|
12
|
+
} = require('../lib/outcomes');
|
|
13
|
+
|
|
14
|
+
const { b, teal, peach, sage, slate, cream, ember, green } = ui;
|
|
15
|
+
|
|
16
|
+
const OUTCOME_COLOR = {
|
|
17
|
+
kept: green, reverted: ember, superseded: peach, failed: ember,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function fmtAgo(ts) {
|
|
21
|
+
const ms = Date.now() - new Date(ts).getTime();
|
|
22
|
+
const mins = Math.floor(ms / 60000);
|
|
23
|
+
if (mins < 60) return `${mins}m`;
|
|
24
|
+
const hrs = Math.floor(mins / 60);
|
|
25
|
+
if (hrs < 24) return `${hrs}h`;
|
|
26
|
+
return `${Math.floor(hrs / 24)}d`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Pad the raw word before coloring — ANSI codes break padEnd's width math
|
|
30
|
+
function outcomeBadge(d, width = 10) {
|
|
31
|
+
if (!d.outcome) return slate('pending'.padEnd(width));
|
|
32
|
+
return OUTCOME_COLOR[d.outcome](d.outcome.padEnd(width));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function showBypasses() {
|
|
36
|
+
const bypasses = bypassStats();
|
|
37
|
+
if (bypasses.total === 0) return;
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log(` ${b(cream('Bypasses'))} ${slate('— why the front door got skipped')}`);
|
|
40
|
+
for (const [reason, count] of Object.entries(bypasses.byReason).sort((a, b) => b[1] - a[1])) {
|
|
41
|
+
console.log(` ${cream(String(count).padStart(3))} ${sage(reason)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function showStats() {
|
|
46
|
+
const stats = outcomeStats();
|
|
47
|
+
const labeled = stats.total - stats.pending;
|
|
48
|
+
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(` ${b(cream('Outcomes'))} ${slate('— what your decisions became')}`);
|
|
51
|
+
ui.divider('line');
|
|
52
|
+
|
|
53
|
+
if (stats.total === 0) {
|
|
54
|
+
console.log(` ${sage('Nothing recorded yet.')}`);
|
|
55
|
+
console.log(` ${slate('Work through a phewsh session — every routed action records a decision.')}`);
|
|
56
|
+
console.log(` ${slate('Label them 1-4 as you learn what held up.')}`);
|
|
57
|
+
showBypasses();
|
|
58
|
+
console.log('');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(` ${cream(String(stats.total))} ${sage('decisions')} ${slate('·')} ${cream(String(labeled))} ${sage('labeled')} ${slate('·')} ${cream(String(stats.pending))} ${sage('pending')}`);
|
|
63
|
+
if (labeled > 0) {
|
|
64
|
+
console.log(` ${green(stats.kept + ' kept')} ${slate('·')} ${ember(stats.reverted + ' reverted')} ${slate('·')} ${peach(stats.superseded + ' superseded')} ${slate('·')} ${ember(stats.failed + ' failed')}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const routes = Object.entries(stats.byRoute);
|
|
68
|
+
if (routes.length > 0) {
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(` ${b(cream('By route'))}`);
|
|
71
|
+
for (const [route, r] of routes.sort((a, b) => b[1].total - a[1].total)) {
|
|
72
|
+
const rate = r.total > 0 ? Math.round((r.kept / r.total) * 100) : 0;
|
|
73
|
+
console.log(` ${cream(route.padEnd(14))} ${sage(`${r.total} labeled`)} ${slate('·')} ${green(`${rate}% kept`)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const modes = Object.entries(stats.byMode);
|
|
78
|
+
if (modes.length > 0) {
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(` ${b(cream('By mode'))}`);
|
|
81
|
+
for (const [mode, m] of modes.sort((a, b) => b[1].total - a[1].total)) {
|
|
82
|
+
const rate = m.total > 0 ? Math.round((m.kept / m.total) * 100) : 0;
|
|
83
|
+
console.log(` ${cream(mode.padEnd(14))} ${sage(`${m.total} labeled`)} ${slate('·')} ${green(`${rate}% kept`)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const recent = recentDecisions(8);
|
|
88
|
+
if (recent.length > 0) {
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(` ${b(cream('Recent'))}`);
|
|
91
|
+
for (const d of recent) {
|
|
92
|
+
console.log(` ${slate(d.id.padEnd(10))} ${outcomeBadge(d)} ${slate(fmtAgo(d.ts).padStart(3))} ${sage(d.summary.slice(0, 56))}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
showBypasses();
|
|
97
|
+
|
|
98
|
+
if (stats.pending > 0) {
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(` ${sage('Label pending decisions:')} ${cream('phewsh outcomes label')}`);
|
|
101
|
+
}
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function labelInteractive() {
|
|
106
|
+
const pending = pendingDecisions();
|
|
107
|
+
if (pending.length === 0) {
|
|
108
|
+
console.log(`\n ${teal('●')} ${sage('No pending decisions — everything is labeled.')}\n`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(` ${b(cream(`${pending.length} pending decision${pending.length !== 1 ? 's' : ''}`))}`);
|
|
114
|
+
console.log(` ${slate('1 kept · 2 reverted · 3 superseded · 4 failed · enter = skip · q = quit')}`);
|
|
115
|
+
ui.divider('line');
|
|
116
|
+
|
|
117
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
118
|
+
let i = 0;
|
|
119
|
+
|
|
120
|
+
const next = () => {
|
|
121
|
+
if (i >= pending.length) {
|
|
122
|
+
rl.close();
|
|
123
|
+
console.log(`\n ${teal('●')} ${sage('Done.')}\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const d = pending[i];
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(` ${slate(d.id)} ${sage(fmtAgo(d.ts) + ' ago')} ${slate('·')} ${cream(d.route)} ${slate('·')} ${sage(d.project)}`);
|
|
129
|
+
console.log(` ${cream(d.summary)}`);
|
|
130
|
+
rl.question(` ${teal('>')} `, (answer) => {
|
|
131
|
+
const a = answer.trim().toLowerCase();
|
|
132
|
+
if (a === 'q') {
|
|
133
|
+
rl.close();
|
|
134
|
+
console.log(`\n ${sage('Stopped — the rest stay pending.')}\n`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const idx = parseInt(a, 10);
|
|
138
|
+
if (idx >= 1 && idx <= 4) {
|
|
139
|
+
labelOutcome(d.id, OUTCOMES[idx - 1]);
|
|
140
|
+
console.log(` ${teal('●')} ${OUTCOME_COLOR[OUTCOMES[idx - 1]](OUTCOMES[idx - 1])}`);
|
|
141
|
+
}
|
|
142
|
+
i++;
|
|
143
|
+
next();
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
next();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = function outcomes() {
|
|
150
|
+
const args = process.argv.slice(3);
|
|
151
|
+
const sub = args[0];
|
|
152
|
+
|
|
153
|
+
if (sub === 'label') {
|
|
154
|
+
const id = args[1];
|
|
155
|
+
const outcome = args[2];
|
|
156
|
+
if (id && outcome) {
|
|
157
|
+
try {
|
|
158
|
+
const d = labelOutcome(id, outcome);
|
|
159
|
+
if (d) console.log(`\n ${teal('●')} ${sage('Labeled')} ${cream(d.summary.slice(0, 50))} ${slate('→')} ${OUTCOME_COLOR[outcome](outcome)}\n`);
|
|
160
|
+
else console.log(`\n ${ember('!')} ${sage('No unique decision matching')} ${cream(id)}\n`);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.log(`\n ${ember('!')} ${sage(err.message)}\n`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
labelInteractive();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
showStats();
|
|
171
|
+
};
|
package/commands/session.js
CHANGED
|
@@ -16,6 +16,9 @@ const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
|
16
16
|
const { select, refreshSession: refreshSess } = require('../lib/supabase');
|
|
17
17
|
const { readPPS } = require('../lib/pps');
|
|
18
18
|
const { push, pull, ensureValidToken } = require('./sync');
|
|
19
|
+
const { HARNESSES, listHarnesses, runViaHarness } = require('../lib/harnesses');
|
|
20
|
+
const { recordDecision, labelOutcome, pendingDecisions, OUTCOMES } = require('../lib/outcomes');
|
|
21
|
+
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
19
22
|
|
|
20
23
|
// Brand palette shortcuts
|
|
21
24
|
const { b, d, w, g, green, cyan, yellow,
|
|
@@ -98,6 +101,43 @@ const MODELS = {
|
|
|
98
101
|
|
|
99
102
|
const DEFAULT_MODEL = 'claude-sonnet';
|
|
100
103
|
|
|
104
|
+
// ── Routing: where plain typed input goes ─────────────────────────────────
|
|
105
|
+
// A route is either an installed harness (your existing subscription — no
|
|
106
|
+
// API key needed in phewsh) or the direct API (your key). Precedence:
|
|
107
|
+
// explicit config.defaultRoute → API key if set → first installed harness.
|
|
108
|
+
|
|
109
|
+
function resolveRoute(config, harnesses) {
|
|
110
|
+
const installed = harnesses.filter(h => h.installed);
|
|
111
|
+
const preferred = config?.defaultRoute;
|
|
112
|
+
if (preferred === 'api' && config?.apiKey) return { type: 'api' };
|
|
113
|
+
if (preferred && installed.some(h => h.id === preferred)) {
|
|
114
|
+
return { type: 'harness', id: preferred };
|
|
115
|
+
}
|
|
116
|
+
if (config?.apiKey) return { type: 'api' };
|
|
117
|
+
if (installed.length > 0) return { type: 'harness', id: installed[0].id };
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function routeLabel(route, config) {
|
|
122
|
+
if (!route) return 'no route — /setup';
|
|
123
|
+
if (route.type === 'api') {
|
|
124
|
+
return `API (${config?.provider === 'openrouter' ? 'OpenRouter' : 'Anthropic'} key)`;
|
|
125
|
+
}
|
|
126
|
+
const h = HARNESSES[route.id];
|
|
127
|
+
return `${h.label} (your ${h.auth.split(' / ')[0].toLowerCase()})`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Intent modes: "What are you trying to do?" ────────────────────────────
|
|
131
|
+
// Picked by number at the start of a session. Shapes the system prompt; the
|
|
132
|
+
// route stays whatever it is. Mode 5 is a route switcher, handled inline.
|
|
133
|
+
|
|
134
|
+
const INTENT_MODES = {
|
|
135
|
+
1: { id: 'build', label: 'Build', hint: 'The user is in execution mode. Bias toward concrete next steps, working code, and shipping. Flag scope creep — it is their most common failure pattern.' },
|
|
136
|
+
2: { id: 'research', label: 'Research', hint: 'The user is exploring. Compare options honestly, surface trade-offs, and say what you would pick and why. Do not pad.' },
|
|
137
|
+
3: { id: 'decide', label: 'Decide', hint: 'The user needs to make a decision. Force clarity: name the actual decision, the options, the constraints from .intent/, and give one recommendation with reasoning. Small constrained choices beat big vague ones.' },
|
|
138
|
+
4: { id: 'review', label: 'Review', hint: 'The user wants critical review. Find what is wrong or risky before praising anything. Be specific about severity.' },
|
|
139
|
+
};
|
|
140
|
+
|
|
101
141
|
function loadConfig() {
|
|
102
142
|
if (!fs.existsSync(CONFIG_PATH)) return null;
|
|
103
143
|
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
@@ -134,6 +174,17 @@ function buildSystemPrompt(intentFiles) {
|
|
|
134
174
|
return `${base}\n\nThe user has structured intent artifacts for this project. Use them as primary context — stay aligned with their vision, plan, and next actions.\n\n${sections}`;
|
|
135
175
|
}
|
|
136
176
|
|
|
177
|
+
// Harness CLIs are one-shot — fold the recent conversation into the prompt
|
|
178
|
+
// so a session through Claude Code / Codex still feels continuous.
|
|
179
|
+
function buildHarnessPrompt(messages, input) {
|
|
180
|
+
const tail = messages.slice(-6);
|
|
181
|
+
if (tail.length === 0) return input;
|
|
182
|
+
const transcript = tail
|
|
183
|
+
.map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content.slice(0, 1500)}`)
|
|
184
|
+
.join('\n\n');
|
|
185
|
+
return `Conversation so far:\n\n${transcript}\n\n---\n\nUser: ${input}\n\nRespond to the last user message.`;
|
|
186
|
+
}
|
|
187
|
+
|
|
137
188
|
async function streamChat(apiKey, messages, systemPrompt, modelId) {
|
|
138
189
|
const body = { model: modelId, max_tokens: 2048, messages, stream: true };
|
|
139
190
|
if (systemPrompt) body.system = systemPrompt;
|
|
@@ -207,22 +258,22 @@ async function main() {
|
|
|
207
258
|
let totalPromptTokens = 0;
|
|
208
259
|
let totalCompletionTokens = 0;
|
|
209
260
|
|
|
261
|
+
// ── Detect capabilities, resolve the route ──────────────
|
|
262
|
+
const harnesses = listHarnesses();
|
|
263
|
+
const installedHarnesses = harnesses.filter(h => h.installed);
|
|
264
|
+
let route = resolveRoute(config, harnesses);
|
|
265
|
+
let sessionMode = null; // INTENT_MODES id once picked
|
|
266
|
+
let awaitingOutcome = null; // decision id eligible for 1-4 labeling
|
|
267
|
+
let decisionsThisSession = 0;
|
|
268
|
+
|
|
210
269
|
// ── The Exhale: animated brand reveal ──────────────────
|
|
211
270
|
await ui.brandReveal();
|
|
212
271
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
console.log(` ${b(cream('Welcome to phewsh.'))}`);
|
|
216
|
-
console.log('');
|
|
217
|
-
console.log(` ${teal('/key')} ${sage('Set your API key (takes 10 seconds)')}`);
|
|
218
|
-
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
219
|
-
console.log(` ${teal('/tour')} ${sage('See what this does (no key needed)')}`);
|
|
220
|
-
console.log('');
|
|
221
|
-
} else if (!config.apiKey.startsWith('sk-')) {
|
|
222
|
-
console.log(` ${ember('!')} ${sage('Stored API key looks invalid.')}`);
|
|
223
|
-
console.log(` ${sage('Run')} ${cream('/key')} ${sage('to set a new one')}`);
|
|
272
|
+
if (config?.apiKey && !config.apiKey.startsWith('sk-')) {
|
|
273
|
+
console.log(` ${ember('!')} ${sage('Stored API key looks invalid — ignoring it.')} ${slate('/key to replace')}`);
|
|
224
274
|
console.log('');
|
|
225
275
|
config.apiKey = null;
|
|
276
|
+
route = resolveRoute(config, harnesses);
|
|
226
277
|
}
|
|
227
278
|
|
|
228
279
|
// ── Project status (compact) ────────────────────────────
|
|
@@ -232,9 +283,23 @@ async function main() {
|
|
|
232
283
|
} else {
|
|
233
284
|
statusParts.push(slate('no .intent/ — run /init'));
|
|
234
285
|
}
|
|
235
|
-
statusParts.push(sage(
|
|
286
|
+
statusParts.push(sage('via ' + routeLabel(route, config)));
|
|
236
287
|
console.log(` ${statusParts.join(slate(' · '))}`);
|
|
237
288
|
|
|
289
|
+
// Capabilities: what's installed on this machine, no setup required
|
|
290
|
+
if (installedHarnesses.length > 0) {
|
|
291
|
+
const caps = harnesses.map(h =>
|
|
292
|
+
h.installed ? `${teal('✓')} ${sage(h.label)}` : slate(`✗ ${h.label}`)
|
|
293
|
+
).join(slate(' · '));
|
|
294
|
+
console.log(` ${caps}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Decisions from past sessions still waiting on an outcome
|
|
298
|
+
const pendingPast = pendingDecisions();
|
|
299
|
+
if (pendingPast.length > 0) {
|
|
300
|
+
console.log(` ${peach('◌')} ${sage(`${pendingPast.length} decision${pendingPast.length !== 1 ? 's' : ''} awaiting outcome`)} ${slate('— /outcomes to label')}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
238
303
|
// Sync status (one-line, non-blocking)
|
|
239
304
|
if (config?.supabaseUserId && intentFiles.length > 0) {
|
|
240
305
|
const syncResult = await Promise.race([
|
|
@@ -253,10 +318,20 @@ async function main() {
|
|
|
253
318
|
}
|
|
254
319
|
|
|
255
320
|
console.log('');
|
|
256
|
-
if (!
|
|
257
|
-
|
|
321
|
+
if (!route) {
|
|
322
|
+
// Nothing to route through: no key, no agent CLIs found on this machine.
|
|
323
|
+
console.log(` ${b(cream('Welcome to phewsh.'))}`);
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log(` ${sage('No agent CLI found (Claude Code, Codex, Gemini, Cursor, OpenCode)')}`);
|
|
326
|
+
console.log(` ${sage('and no API key set. Either one gets you running:')}`);
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log(` ${teal('/key')} ${sage('Set an API key (10 seconds)')}`);
|
|
329
|
+
console.log(` ${teal('/tour')} ${sage('See what this does (nothing needed)')}`);
|
|
330
|
+
console.log(` ${slate('Or install Claude Code / Codex — phewsh uses their login automatically.')}`);
|
|
258
331
|
} else {
|
|
259
|
-
console.log(` ${
|
|
332
|
+
console.log(` ${b(cream('What are you trying to do?'))}`);
|
|
333
|
+
console.log(` ${teal('1')} ${sage('Build')} ${slate('·')} ${teal('2')} ${sage('Research')} ${slate('·')} ${teal('3')} ${sage('Decide')} ${slate('·')} ${teal('4')} ${sage('Review')} ${slate('·')} ${teal('5')} ${sage('Ask another model')}`);
|
|
334
|
+
console.log(` ${slate('pick a number, or just type — both work')}`);
|
|
260
335
|
}
|
|
261
336
|
console.log('');
|
|
262
337
|
|
|
@@ -277,6 +352,49 @@ async function main() {
|
|
|
277
352
|
return;
|
|
278
353
|
}
|
|
279
354
|
|
|
355
|
+
// A bare 1-4 right after a routed action labels its outcome
|
|
356
|
+
if (awaitingOutcome && /^[1-4]$/.test(input)) {
|
|
357
|
+
const outcome = OUTCOMES[parseInt(input, 10) - 1];
|
|
358
|
+
try {
|
|
359
|
+
labelOutcome(awaitingOutcome, outcome);
|
|
360
|
+
const color = outcome === 'kept' ? green : outcome === 'superseded' ? peach : ember;
|
|
361
|
+
console.log(` ${teal('●')} ${sage('outcome:')} ${color(outcome)}`);
|
|
362
|
+
} catch { /* decision vanished — nothing to do */ }
|
|
363
|
+
awaitingOutcome = null;
|
|
364
|
+
console.log('');
|
|
365
|
+
rl.prompt();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// A bare 1-5 on an empty conversation picks an intent mode
|
|
370
|
+
if (messages.length === 0 && !awaitingOutcome && /^[1-5]$/.test(input)) {
|
|
371
|
+
const n = parseInt(input, 10);
|
|
372
|
+
if (n === 5) {
|
|
373
|
+
console.log('');
|
|
374
|
+
console.log(` ${b(cream('Ask another model — switch the route'))}`);
|
|
375
|
+
for (const h of harnesses) {
|
|
376
|
+
if (h.installed) console.log(` ${teal('/use ' + h.id.padEnd(12))} ${sage(h.label)} ${slate('(' + h.auth + ')')}`);
|
|
377
|
+
}
|
|
378
|
+
if (config?.apiKey) console.log(` ${teal('/use api'.padEnd(17))} ${sage('Direct API')} ${slate('(your key)')}`);
|
|
379
|
+
console.log('');
|
|
380
|
+
rl.prompt();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
sessionMode = INTENT_MODES[n].id;
|
|
384
|
+
console.log('');
|
|
385
|
+
console.log(` ${teal('●')} ${cream(INTENT_MODES[n].label)} ${sage('mode · via ' + routeLabel(route, config))}`);
|
|
386
|
+
if (sessionMode === 'review' && route?.id !== 'codex' && installedHarnesses.some(h => h.id === 'codex')) {
|
|
387
|
+
console.log(` ${slate('tip: a second model reviews more honestly — /use codex')}`);
|
|
388
|
+
}
|
|
389
|
+
if (sessionMode === 'build' && route?.type === 'harness') {
|
|
390
|
+
console.log(` ${slate('tip: when this needs real file edits, /work drops you into ' + HARNESSES[route.id].label + ' and brings you back')}`);
|
|
391
|
+
}
|
|
392
|
+
console.log(` ${sage('Describe it.')}`);
|
|
393
|
+
console.log('');
|
|
394
|
+
rl.prompt();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
280
398
|
// Slash commands
|
|
281
399
|
if (input.startsWith('/')) {
|
|
282
400
|
const parts = input.slice(1).split(/\s+/);
|
|
@@ -291,6 +409,10 @@ async function main() {
|
|
|
291
409
|
}
|
|
292
410
|
console.log('');
|
|
293
411
|
console.log(` ${sage('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
|
|
412
|
+
if (decisionsThisSession > 0) {
|
|
413
|
+
const stillPending = pendingDecisions().length;
|
|
414
|
+
console.log(` ${sage(decisionsThisSession + ' decision' + (decisionsThisSession !== 1 ? 's' : '') + ' recorded')}${stillPending > 0 ? slate(` · ${stillPending} awaiting outcome — phewsh outcomes label`) : ''}`);
|
|
415
|
+
}
|
|
294
416
|
console.log('');
|
|
295
417
|
process.exit(0);
|
|
296
418
|
}
|
|
@@ -316,16 +438,23 @@ async function main() {
|
|
|
316
438
|
console.log(` ${teal('/serve')} ${sage('Execution bridge for phewsh.com/intent')}`);
|
|
317
439
|
console.log(` ${teal('/sync')} ${sage('Check sync status')}`);
|
|
318
440
|
console.log('');
|
|
441
|
+
console.log(` ${cream('route — where your typing goes')}`);
|
|
442
|
+
console.log(` ${teal('/use')} ${slate('<route>')} ${sage('Switch: claude-code, codex, gemini, cursor, opencode, api')}`);
|
|
443
|
+
console.log(` ${teal('/harnesses')} ${sage('Agent CLIs detected on this machine')}`);
|
|
444
|
+
console.log(` ${teal('/provider')} ${sage('Current route + what\'s available')}`);
|
|
445
|
+
console.log(` ${teal('/outcomes')} ${sage('Decision record — kept/reverted/superseded/failed')}`);
|
|
446
|
+
console.log('');
|
|
319
447
|
console.log(` ${cream('session')}`);
|
|
448
|
+
console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('Hand off to interactive Claude Code/Codex — outcome on return')}`);
|
|
320
449
|
console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
|
|
321
450
|
console.log(` ${teal('/clear')} ${sage('Clear conversation')}`);
|
|
322
451
|
console.log(` ${teal('/status')} ${sage('Session stats')}`);
|
|
323
452
|
console.log(` ${teal('/quit')} ${sage('Exit')}`);
|
|
324
453
|
console.log('');
|
|
325
454
|
console.log(` ${cream('configure')}`);
|
|
326
|
-
console.log(` ${teal('/key')} ${sage('Set API key')}`);
|
|
455
|
+
console.log(` ${teal('/key')} ${sage('Set API key (optional — harnesses need none)')}`);
|
|
327
456
|
console.log(` ${teal('/login')} ${sage('Identity + cloud sync')}`);
|
|
328
|
-
console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model (sonnet, opus, haiku)')}`);
|
|
457
|
+
console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch API model (sonnet, opus, haiku)')}`);
|
|
329
458
|
console.log(` ${teal('/update')} ${sage('Update phewsh')}`);
|
|
330
459
|
console.log(` ${teal('/tour')} ${sage('Quick walkthrough')}`);
|
|
331
460
|
console.log('');
|
|
@@ -388,10 +517,11 @@ async function main() {
|
|
|
388
517
|
['Tokens', `${totalPromptTokens} in → ${totalCompletionTokens} out`],
|
|
389
518
|
['Project', projectName, 'cyan'],
|
|
390
519
|
['Context', intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none', intentFiles.length > 0 ? 'green' : 'yellow'],
|
|
391
|
-
['
|
|
392
|
-
['
|
|
520
|
+
['Route', routeLabel(route, config), 'green'],
|
|
521
|
+
['Mode', sessionMode || 'none'],
|
|
522
|
+
['Decisions', `${decisionsThisSession} this session`],
|
|
393
523
|
['User', config?.email || slate('not logged in')],
|
|
394
|
-
['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '...' : 'not set', config?.apiKey ? 'green' : 'yellow'],
|
|
524
|
+
['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '...' : 'not set — optional', config?.apiKey ? 'green' : 'yellow'],
|
|
395
525
|
]);
|
|
396
526
|
rl.prompt();
|
|
397
527
|
return;
|
|
@@ -587,6 +717,20 @@ async function main() {
|
|
|
587
717
|
return;
|
|
588
718
|
}
|
|
589
719
|
|
|
720
|
+
if (cmd === 'setup') {
|
|
721
|
+
try {
|
|
722
|
+
const { execSync } = require('child_process');
|
|
723
|
+
execSync(`node ${path.join(__dirname, '..', 'bin', 'phewsh.js')} setup`, { stdio: 'inherit' });
|
|
724
|
+
config = loadConfig();
|
|
725
|
+
route = resolveRoute(config, harnesses);
|
|
726
|
+
console.log(` ${teal('●')} ${sage('Route now:')} ${cream(routeLabel(route, config))}`);
|
|
727
|
+
} catch (err) {
|
|
728
|
+
console.error(` ${sage('Setup failed:')} ${err.message}`);
|
|
729
|
+
}
|
|
730
|
+
rl.prompt();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
590
734
|
if (cmd === 'key') {
|
|
591
735
|
if (cmdArg) {
|
|
592
736
|
const apiKey = cmdArg.trim();
|
|
@@ -606,6 +750,7 @@ async function main() {
|
|
|
606
750
|
saveConfig(config);
|
|
607
751
|
console.log(` ${teal('●')} ${sage('API key saved. You\'re ready — just type.')}\n`);
|
|
608
752
|
}
|
|
753
|
+
if (!route || route.type !== 'harness') route = resolveRoute(config, harnesses);
|
|
609
754
|
rl.prompt();
|
|
610
755
|
return;
|
|
611
756
|
}
|
|
@@ -637,6 +782,7 @@ async function main() {
|
|
|
637
782
|
else config.provider = 'anthropic';
|
|
638
783
|
saveConfig(config);
|
|
639
784
|
console.log(`\n ${teal('●')} ${sage('API key saved. You\'re ready — just type naturally.')}\n`);
|
|
785
|
+
if (!route || route.type !== 'harness') route = resolveRoute(config, harnesses);
|
|
640
786
|
}
|
|
641
787
|
rl.prompt();
|
|
642
788
|
});
|
|
@@ -678,14 +824,73 @@ async function main() {
|
|
|
678
824
|
return;
|
|
679
825
|
}
|
|
680
826
|
|
|
681
|
-
if (cmd === 'provider') {
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
['
|
|
687
|
-
|
|
688
|
-
]);
|
|
827
|
+
if (cmd === 'provider' || cmd === 'route') {
|
|
828
|
+
const rows = [
|
|
829
|
+
['Route', routeLabel(route, config), 'green'],
|
|
830
|
+
];
|
|
831
|
+
for (const h of harnesses) {
|
|
832
|
+
rows.push([h.label, h.installed ? `installed (${h.auth})` : 'not installed', h.installed ? 'green' : undefined]);
|
|
833
|
+
}
|
|
834
|
+
rows.push(['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '... (' + (config.provider || 'anthropic') + ')' : 'not set — optional', config?.apiKey ? 'green' : 'yellow']);
|
|
835
|
+
if (route?.type === 'api') rows.push(['Model', MODELS[currentModel].name, 'cyan']);
|
|
836
|
+
ui.statusPanel('Provider', rows);
|
|
837
|
+
console.log(` ${slate('switch:')} ${cream('/use <' + harnesses.filter(h => h.installed).map(h => h.id).concat(config?.apiKey ? ['api'] : []).join('|') + '>')}`);
|
|
838
|
+
console.log('');
|
|
839
|
+
rl.prompt();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (cmd === 'use') {
|
|
844
|
+
if (!cmdArg) {
|
|
845
|
+
console.log(` ${sage('Current route:')} ${cream(routeLabel(route, config))}`);
|
|
846
|
+
console.log(` ${sage('Usage:')} ${cream('/use <claude-code|codex|gemini|cursor|opencode|api>')}`);
|
|
847
|
+
rl.prompt();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const target = cmdArg.trim().toLowerCase();
|
|
851
|
+
if (target === 'api') {
|
|
852
|
+
if (!config?.apiKey) {
|
|
853
|
+
console.log(` ${ember('!')} ${sage('No API key set — run /key first.')}`);
|
|
854
|
+
} else {
|
|
855
|
+
route = { type: 'api' };
|
|
856
|
+
console.log(` ${teal('●')} ${sage('Routing via')} ${cream(routeLabel(route, config))}`);
|
|
857
|
+
}
|
|
858
|
+
} else if (HARNESSES[target]) {
|
|
859
|
+
if (!harnesses.find(h => h.id === target)?.installed) {
|
|
860
|
+
console.log(` ${ember('!')} ${sage(HARNESSES[target].label + ' is not installed on this machine.')}`);
|
|
861
|
+
} else {
|
|
862
|
+
route = { type: 'harness', id: target };
|
|
863
|
+
console.log(` ${teal('●')} ${sage('Routing via')} ${cream(routeLabel(route, config))} ${slate('— no API key, your subscription')}`);
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
console.log(` ${sage('Unknown route. Options:')} ${cream(Object.keys(HARNESSES).join(', ') + ', api')}`);
|
|
867
|
+
}
|
|
868
|
+
console.log(` ${slate('make it stick across sessions: phewsh setup')}`);
|
|
869
|
+
rl.prompt();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (cmd === 'harnesses' || cmd === 'agents') {
|
|
874
|
+
console.log('');
|
|
875
|
+
console.log(` ${b(cream('Agent CLIs on this machine'))} ${slate('— each carries its own login, no API key')}`);
|
|
876
|
+
ui.divider('line');
|
|
877
|
+
for (const h of harnesses) {
|
|
878
|
+
const active = route?.type === 'harness' && route.id === h.id ? ` ${teal('● active')}` : '';
|
|
879
|
+
const status = h.installed ? green('installed') : slate('not installed');
|
|
880
|
+
console.log(` ${cream(h.id.padEnd(12))} ${sage(h.label.padEnd(14))} ${status}${active}`);
|
|
881
|
+
}
|
|
882
|
+
console.log('');
|
|
883
|
+
rl.prompt();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (cmd === 'outcomes') {
|
|
888
|
+
try {
|
|
889
|
+
// execSync so the labeling prompt owns stdin while it runs
|
|
890
|
+
const { execSync } = require('child_process');
|
|
891
|
+
const outcomesArgs = cmdArg ? ' ' + cmdArg : '';
|
|
892
|
+
execSync(`node ${path.join(__dirname, '..', 'bin', 'phewsh.js')} outcomes${outcomesArgs}`, { stdio: 'inherit' });
|
|
893
|
+
} catch { /* user quit mid-labeling — fine */ }
|
|
689
894
|
rl.prompt();
|
|
690
895
|
return;
|
|
691
896
|
}
|
|
@@ -759,14 +964,69 @@ async function main() {
|
|
|
759
964
|
return;
|
|
760
965
|
}
|
|
761
966
|
|
|
967
|
+
if (cmd === 'work') {
|
|
968
|
+
// Real work needs the real harness. Hand the terminal over to an
|
|
969
|
+
// interactive session, take it back when they exit, ask the outcome.
|
|
970
|
+
// phewsh stays the front door AND the return point.
|
|
971
|
+
const target = cmdArg?.trim().toLowerCase() || (route?.type === 'harness' ? route.id : 'claude-code');
|
|
972
|
+
const h = HARNESSES[target];
|
|
973
|
+
if (!h) {
|
|
974
|
+
console.log(` ${sage('Unknown harness. Options:')} ${cream(Object.keys(HARNESSES).join(', '))}`);
|
|
975
|
+
rl.prompt();
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (!harnesses.find(x => x.id === target)?.installed) {
|
|
979
|
+
console.log(` ${ember('!')} ${sage(h.label + ' is not installed on this machine.')}`);
|
|
980
|
+
rl.prompt();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const decisionId = recordDecision({
|
|
984
|
+
project: projectName,
|
|
985
|
+
route: target,
|
|
986
|
+
mode: sessionMode,
|
|
987
|
+
summary: `interactive ${h.label} session in ${projectName}`,
|
|
988
|
+
});
|
|
989
|
+
decisionsThisSession++;
|
|
990
|
+
console.log('');
|
|
991
|
+
console.log(` ${teal('●')} ${sage('Handing the terminal to')} ${cream(h.label)} ${slate('— exit to come back to phewsh')}`);
|
|
992
|
+
if (fs.existsSync(path.join(process.cwd(), 'CLAUDE.md')) || intentFiles.length > 0) {
|
|
993
|
+
console.log(` ${slate('your .intent/ context rides along via CLAUDE.md')}`);
|
|
994
|
+
}
|
|
995
|
+
console.log('');
|
|
996
|
+
rl.pause();
|
|
997
|
+
const { spawnSync } = require('child_process');
|
|
998
|
+
const res = spawnSync(h.bin, [], { stdio: 'inherit' });
|
|
999
|
+
rl.resume();
|
|
1000
|
+
recordSessionEvent(target, projectName, 'task_complete', {
|
|
1001
|
+
taskId: decisionId, success: res.status === 0, summary: `interactive ${h.label} session`,
|
|
1002
|
+
});
|
|
1003
|
+
awaitingOutcome = decisionId;
|
|
1004
|
+
console.log('');
|
|
1005
|
+
console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing')}`);
|
|
1006
|
+
console.log('');
|
|
1007
|
+
rl.prompt();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
762
1011
|
if (cmd === 'run') {
|
|
763
1012
|
if (!cmdArg) {
|
|
764
1013
|
console.log(` ${sage('Usage:')} ${cream('/run <prompt>')}`);
|
|
765
1014
|
rl.prompt();
|
|
766
1015
|
return;
|
|
767
1016
|
}
|
|
1017
|
+
if (route?.type === 'harness') {
|
|
1018
|
+
try {
|
|
1019
|
+
await runViaHarness(route.id, systemPrompt, cmdArg);
|
|
1020
|
+
console.log(slate(` via ${HARNESSES[route.id].label} · one-shot, no history`));
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
console.error(`\n ${ember('!')} ${err.message}\n`);
|
|
1023
|
+
}
|
|
1024
|
+
console.log('');
|
|
1025
|
+
rl.prompt();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
768
1028
|
if (!config?.apiKey) {
|
|
769
|
-
console.log(` ${ember('!')} ${sage('No API key.
|
|
1029
|
+
console.log(` ${ember('!')} ${sage('No API key and no agent CLI installed. /key or install Claude Code.')}`);
|
|
770
1030
|
rl.prompt();
|
|
771
1031
|
return;
|
|
772
1032
|
}
|
|
@@ -803,10 +1063,51 @@ async function main() {
|
|
|
803
1063
|
return;
|
|
804
1064
|
}
|
|
805
1065
|
|
|
806
|
-
// Regular input →
|
|
807
|
-
if (!
|
|
1066
|
+
// Regular input → route it (harness = your subscription, api = your key)
|
|
1067
|
+
if (!route) {
|
|
808
1068
|
console.log('');
|
|
809
|
-
console.log(` ${sage('
|
|
1069
|
+
console.log(` ${sage('No route yet —')} ${cream('/key')} ${sage('to set an API key, or install Claude Code / Codex')}`);
|
|
1070
|
+
console.log(` ${slate('phewsh uses installed agent CLIs automatically, no key needed.')}`);
|
|
1071
|
+
console.log('');
|
|
1072
|
+
rl.prompt();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const modeHint = sessionMode
|
|
1077
|
+
? Object.values(INTENT_MODES).find(m => m.id === sessionMode)?.hint
|
|
1078
|
+
: null;
|
|
1079
|
+
const fullSystem = modeHint ? `${systemPrompt}\n\n${modeHint}` : systemPrompt;
|
|
1080
|
+
|
|
1081
|
+
// Every routed action is a decision — recorded before it runs,
|
|
1082
|
+
// labeled (1-4) when the outcome is known.
|
|
1083
|
+
const decisionId = recordDecision({
|
|
1084
|
+
project: projectName,
|
|
1085
|
+
route: route.type === 'api' ? 'api' : route.id,
|
|
1086
|
+
mode: sessionMode,
|
|
1087
|
+
summary: input,
|
|
1088
|
+
});
|
|
1089
|
+
decisionsThisSession++;
|
|
1090
|
+
|
|
1091
|
+
if (route.type === 'harness') {
|
|
1092
|
+
try {
|
|
1093
|
+
const output = await runViaHarness(route.id, fullSystem, buildHarnessPrompt(messages, input));
|
|
1094
|
+
messages.push({ role: 'user', content: input });
|
|
1095
|
+
messages.push({ role: 'assistant', content: (output || '').trim() });
|
|
1096
|
+
recordSessionEvent(route.id, projectName, 'task_complete', {
|
|
1097
|
+
taskId: decisionId, success: true, summary: input.slice(0, 140),
|
|
1098
|
+
});
|
|
1099
|
+
awaitingOutcome = decisionId;
|
|
1100
|
+
console.log(slate(` via ${HARNESSES[route.id].label} · outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing`));
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
|
|
1103
|
+
recordSessionEvent(route.id, projectName, 'task_complete', {
|
|
1104
|
+
taskId: decisionId, success: false, summary: input.slice(0, 140),
|
|
1105
|
+
});
|
|
1106
|
+
console.error(`\n ${ember('!')} ${err.message}`);
|
|
1107
|
+
if (installedHarnesses.length > 1 || config?.apiKey) {
|
|
1108
|
+
console.log(` ${slate('switch routes with /use — /provider shows what\'s available')}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
810
1111
|
console.log('');
|
|
811
1112
|
rl.prompt();
|
|
812
1113
|
return;
|
|
@@ -816,15 +1117,16 @@ async function main() {
|
|
|
816
1117
|
console.log('');
|
|
817
1118
|
|
|
818
1119
|
try {
|
|
819
|
-
const result = await streamChat(config.apiKey, messages,
|
|
1120
|
+
const result = await streamChat(config.apiKey, messages, fullSystem, MODELS[currentModel].id);
|
|
820
1121
|
messages.push({ role: 'assistant', content: result.content });
|
|
821
1122
|
|
|
822
1123
|
if (result.promptTokens) totalPromptTokens += result.promptTokens;
|
|
823
1124
|
if (result.completionTokens) totalCompletionTokens += result.completionTokens;
|
|
824
1125
|
|
|
825
1126
|
if (result.promptTokens || result.completionTokens) {
|
|
826
|
-
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
|
|
1127
|
+
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name} · outcome? 1-4 or keep typing`));
|
|
827
1128
|
}
|
|
1129
|
+
awaitingOutcome = decisionId;
|
|
828
1130
|
|
|
829
1131
|
trackSap({
|
|
830
1132
|
userId: config.supabaseUserId,
|
|
@@ -835,6 +1137,7 @@ async function main() {
|
|
|
835
1137
|
accessToken: config.supabaseAccessToken,
|
|
836
1138
|
});
|
|
837
1139
|
} catch (err) {
|
|
1140
|
+
try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
|
|
838
1141
|
console.error(`\n ${err.message}\n`);
|
|
839
1142
|
messages.pop();
|
|
840
1143
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// phewsh setup — guided setup, hermes-style.
|
|
2
|
+
//
|
|
3
|
+
// Detects what's already on the machine (agent CLIs carry their own login),
|
|
4
|
+
// lets you pick the default route, optionally adds an API key. Ends with one
|
|
5
|
+
// instruction: type `phewsh`.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const ui = require('../lib/ui');
|
|
12
|
+
const { HARNESSES, listHarnesses } = require('../lib/harnesses');
|
|
13
|
+
|
|
14
|
+
const { b, teal, sage, slate, cream, ember, green } = ui;
|
|
15
|
+
|
|
16
|
+
const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
|
|
17
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
18
|
+
|
|
19
|
+
function loadConfig() {
|
|
20
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function saveConfig(config) {
|
|
24
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
25
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ask(rl, prompt) {
|
|
29
|
+
return new Promise(resolve => rl.question(prompt, a => resolve(a.trim())));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = async function setup() {
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
const harnesses = listHarnesses();
|
|
35
|
+
const installed = harnesses.filter(h => h.installed);
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(` ${b(cream('phewsh setup'))}`);
|
|
39
|
+
ui.divider('line');
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(` ${sage('phewsh routes your work through tools you already pay for.')}`);
|
|
42
|
+
console.log(` ${sage('No API key required — agent CLIs carry their own login.')}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// ── 1. What's on this machine ─────────────────────────
|
|
46
|
+
console.log(` ${b(cream('Detected on this machine'))}`);
|
|
47
|
+
for (const h of harnesses) {
|
|
48
|
+
const status = h.installed ? green('✓ installed') : slate('✗ not installed');
|
|
49
|
+
console.log(` ${cream(h.label.padEnd(14))} ${status} ${slate('(' + h.auth + ')')}`);
|
|
50
|
+
}
|
|
51
|
+
if (installed.length === 0) {
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(` ${ember('!')} ${sage('No agent CLIs found. Install one (recommended) or use an API key:')}`);
|
|
54
|
+
console.log(` ${slate('Claude Code:')} ${cream('npm install -g @anthropic-ai/claude-code')}`);
|
|
55
|
+
console.log(` ${slate('Codex CLI:')} ${cream('npm install -g @openai/codex')}`);
|
|
56
|
+
}
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
// Agent-run (no TTY): auto-configure instead of asking questions nobody
|
|
60
|
+
// can answer. Pick the first installed harness; humans can change it later.
|
|
61
|
+
if (!process.stdin.isTTY) {
|
|
62
|
+
if (installed.length > 0) {
|
|
63
|
+
config.defaultRoute = installed[0].id;
|
|
64
|
+
saveConfig(config);
|
|
65
|
+
console.log(` ${teal('●')} ${sage('Auto-configured (non-interactive): default route =')} ${cream(installed[0].label)} ${slate('— no API key needed')}`);
|
|
66
|
+
console.log(` ${slate('Change anytime: run `phewsh setup` in your own terminal, or /use inside a session.')}`);
|
|
67
|
+
} else if (config.apiKey) {
|
|
68
|
+
config.defaultRoute = 'api';
|
|
69
|
+
saveConfig(config);
|
|
70
|
+
console.log(` ${teal('●')} ${sage('Auto-configured: default route = API (existing key found)')}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(` ${ember('!')} ${sage('Nothing to configure yet — no agent CLI installed and no API key.')}`);
|
|
73
|
+
console.log(` ${slate('Install Claude Code or Codex (or set a key), then rerun phewsh setup.')}`);
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(` ${sage('Start working:')} ${cream('phewsh')}`);
|
|
77
|
+
console.log('');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
+
|
|
83
|
+
// ── 2. Pick the default route ─────────────────────────
|
|
84
|
+
const options = installed.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} — your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
|
|
85
|
+
options.push({ kind: 'api', id: 'api', label: 'Direct API — bring your own Anthropic/OpenRouter key' });
|
|
86
|
+
|
|
87
|
+
console.log(` ${b(cream('Where should phewsh route your work by default?'))}`);
|
|
88
|
+
options.forEach((o, i) => {
|
|
89
|
+
const current = (config.defaultRoute === o.id) ? ` ${teal('● current')}` : '';
|
|
90
|
+
console.log(` ${teal(String(i + 1))} ${sage(o.label)}${current}`);
|
|
91
|
+
});
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
const answer = await ask(rl, ` ${teal('>')} ${slate(`1-${options.length}, enter = ${options[0] ? '1' : 'skip'}: `)}`);
|
|
95
|
+
const idx = answer === '' ? 0 : parseInt(answer, 10) - 1;
|
|
96
|
+
const choice = options[idx];
|
|
97
|
+
|
|
98
|
+
if (!choice) {
|
|
99
|
+
console.log(` ${slate('Skipped — phewsh will auto-detect each session.')}`);
|
|
100
|
+
} else if (choice.kind === 'harness') {
|
|
101
|
+
config.defaultRoute = choice.id;
|
|
102
|
+
saveConfig(config);
|
|
103
|
+
console.log(` ${teal('●')} ${sage('Default route:')} ${cream(HARNESSES[choice.id].label)} ${slate('— no API key needed')}`);
|
|
104
|
+
} else {
|
|
105
|
+
// ── 3. API key, only if they chose the API route ────
|
|
106
|
+
config.defaultRoute = 'api';
|
|
107
|
+
if (config.apiKey) {
|
|
108
|
+
console.log(` ${teal('●')} ${sage('Default route: API — using your existing key')} ${slate('(' + config.apiKey.slice(0, 8) + '...)')}`);
|
|
109
|
+
saveConfig(config);
|
|
110
|
+
} else {
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(` ${sage('Anthropic:')} ${cream('console.anthropic.com/settings/keys')} ${slate('(sk-ant-...)')}`);
|
|
113
|
+
console.log(` ${sage('OpenRouter:')} ${cream('openrouter.ai/keys')} ${slate('(sk-or-...)')}`);
|
|
114
|
+
const key = await ask(rl, ` ${sage('Paste your API key (enter to skip):')}\n ${teal('>')} `);
|
|
115
|
+
if (key) {
|
|
116
|
+
config.apiKey = key;
|
|
117
|
+
config.provider = key.startsWith('sk-or-') ? 'openrouter' : 'anthropic';
|
|
118
|
+
console.log(` ${teal('●')} ${sage('Key saved.')}`);
|
|
119
|
+
} else {
|
|
120
|
+
delete config.defaultRoute;
|
|
121
|
+
console.log(` ${slate('No key — phewsh will fall back to any installed agent CLI.')}`);
|
|
122
|
+
}
|
|
123
|
+
saveConfig(config);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
rl.close();
|
|
128
|
+
|
|
129
|
+
// ── 4. Done ───────────────────────────────────────────
|
|
130
|
+
console.log('');
|
|
131
|
+
ui.divider('line');
|
|
132
|
+
console.log(` ${teal('●')} ${b(cream('Setup complete.'))}`);
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(` ${sage('Start working:')} ${cream('phewsh')}`);
|
|
135
|
+
console.log(` ${slate('Optional: phewsh login (cloud sync) · phewsh intent --init (.intent/ for a project)')}`);
|
|
136
|
+
console.log('');
|
|
137
|
+
};
|
package/lib/harnesses.js
CHANGED
|
@@ -40,6 +40,7 @@ function listHarnesses() {
|
|
|
40
40
|
/**
|
|
41
41
|
* Run a prompt through a harness, streaming stdout to the terminal.
|
|
42
42
|
* stderr is buffered and only surfaced on failure (codex/gemini chat on it).
|
|
43
|
+
* Resolves with the full stdout text so callers can keep conversation history.
|
|
43
44
|
*/
|
|
44
45
|
function runViaHarness(id, systemPrompt, userPrompt) {
|
|
45
46
|
const h = HARNESSES[id];
|
|
@@ -51,13 +52,14 @@ function runViaHarness(id, systemPrompt, userPrompt) {
|
|
|
51
52
|
// Some harnesses (codex exec, gemini) wait for stdin EOF before running.
|
|
52
53
|
child.stdin.end();
|
|
53
54
|
|
|
55
|
+
let stdout = '';
|
|
54
56
|
let stderr = '';
|
|
55
57
|
process.stdout.write('\n');
|
|
56
|
-
child.stdout.on('data', (d) => process.stdout.write(d));
|
|
58
|
+
child.stdout.on('data', (d) => { process.stdout.write(d); stdout += d.toString(); });
|
|
57
59
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
58
60
|
child.on('close', (code) => {
|
|
59
61
|
process.stdout.write('\n');
|
|
60
|
-
if (code === 0) resolve();
|
|
62
|
+
if (code === 0) resolve(stdout);
|
|
61
63
|
else reject(new Error(`${h.label} exited ${code}${stderr ? `\n ${stderr.trim().split('\n').slice(-3).join('\n ')}` : ''}`));
|
|
62
64
|
});
|
|
63
65
|
child.on('error', (e) => reject(new Error(`Could not run ${h.bin}: ${e.message}`)));
|
package/lib/outcomes.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Outcome-labeled decision history — the dataset PHEWSH accumulates that
|
|
2
|
+
// platform chat logs don't: not just "I did X" but "X was kept / reverted /
|
|
3
|
+
// superseded / failed."
|
|
4
|
+
//
|
|
5
|
+
// Every routed action in a phewsh session records a decision (pending).
|
|
6
|
+
// The user labels it when the outcome is actually known — seconds later or
|
|
7
|
+
// three weeks later. `phewsh outcomes` shows what accumulates.
|
|
8
|
+
//
|
|
9
|
+
// Storage: ~/.phewsh/outcomes/decisions.json (append-only, never capped —
|
|
10
|
+
// this file IS the asset).
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
const OUTCOMES_DIR = path.join(os.homedir(), '.phewsh', 'outcomes');
|
|
17
|
+
const DECISIONS_FILE = path.join(OUTCOMES_DIR, 'decisions.json');
|
|
18
|
+
|
|
19
|
+
const OUTCOMES = ['kept', 'reverted', 'superseded', 'failed'];
|
|
20
|
+
|
|
21
|
+
function load() {
|
|
22
|
+
try { return JSON.parse(fs.readFileSync(DECISIONS_FILE, 'utf-8')); } catch { return []; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function save(decisions) {
|
|
26
|
+
fs.mkdirSync(OUTCOMES_DIR, { recursive: true });
|
|
27
|
+
fs.writeFileSync(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Record a routed action as a pending decision. Returns the decision id. */
|
|
31
|
+
function recordDecision({ project, route, mode, summary }) {
|
|
32
|
+
const decisions = load();
|
|
33
|
+
// Short enough to display whole and retype: 5 timestamp chars + 3 random
|
|
34
|
+
const id = 'd' + Date.now().toString(36).slice(-5) + Math.random().toString(36).slice(2, 5);
|
|
35
|
+
decisions.push({
|
|
36
|
+
id,
|
|
37
|
+
ts: new Date().toISOString(),
|
|
38
|
+
project: project || path.basename(process.cwd()),
|
|
39
|
+
route: route || 'unknown',
|
|
40
|
+
mode: mode || null,
|
|
41
|
+
summary: (summary || '').slice(0, 200),
|
|
42
|
+
outcome: null,
|
|
43
|
+
labeledAt: null,
|
|
44
|
+
});
|
|
45
|
+
save(decisions);
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Label a decision by id (or unambiguous id prefix). Returns the decision or null. */
|
|
50
|
+
function labelOutcome(idOrPrefix, outcome) {
|
|
51
|
+
if (!OUTCOMES.includes(outcome)) {
|
|
52
|
+
throw new Error(`Outcome must be one of: ${OUTCOMES.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
const decisions = load();
|
|
55
|
+
const matches = decisions.filter(d => d.id === idOrPrefix || d.id.startsWith(idOrPrefix));
|
|
56
|
+
if (matches.length !== 1) return null;
|
|
57
|
+
matches[0].outcome = outcome;
|
|
58
|
+
matches[0].labeledAt = new Date().toISOString();
|
|
59
|
+
save(decisions);
|
|
60
|
+
return matches[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Unlabeled decisions, oldest first. */
|
|
64
|
+
function pendingDecisions({ project = null } = {}) {
|
|
65
|
+
return load()
|
|
66
|
+
.filter(d => !d.outcome && (!project || d.project === project))
|
|
67
|
+
.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Most recent decisions (labeled or not), newest first. */
|
|
71
|
+
function recentDecisions(limit = 10, { project = null } = {}) {
|
|
72
|
+
return load()
|
|
73
|
+
.filter(d => !project || d.project === project)
|
|
74
|
+
.sort((a, b) => b.ts.localeCompare(a.ts))
|
|
75
|
+
.slice(0, limit);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The Day-14 view: totals, per-route reliability, per-mode patterns.
|
|
80
|
+
* Only labeled decisions count toward outcome rates — pending is honest noise.
|
|
81
|
+
*/
|
|
82
|
+
function outcomeStats({ project = null } = {}) {
|
|
83
|
+
const all = load().filter(d => !project || d.project === project);
|
|
84
|
+
const labeled = all.filter(d => d.outcome);
|
|
85
|
+
|
|
86
|
+
const stats = {
|
|
87
|
+
total: all.length,
|
|
88
|
+
pending: all.length - labeled.length,
|
|
89
|
+
kept: 0, reverted: 0, superseded: 0, failed: 0,
|
|
90
|
+
byRoute: {},
|
|
91
|
+
byMode: {},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (const d of labeled) {
|
|
95
|
+
stats[d.outcome]++;
|
|
96
|
+
const r = (stats.byRoute[d.route] ||= { total: 0, kept: 0, reverted: 0, superseded: 0, failed: 0 });
|
|
97
|
+
r.total++; r[d.outcome]++;
|
|
98
|
+
if (d.mode) {
|
|
99
|
+
const m = (stats.byMode[d.mode] ||= { total: 0, kept: 0, reverted: 0, superseded: 0, failed: 0 });
|
|
100
|
+
m.total++; m[d.outcome]++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return stats;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Bypasses — the failure dataset ─────────────────────────────────────────
|
|
108
|
+
// Every time the user opens Claude Code (or anything) directly instead of
|
|
109
|
+
// phewsh, the reason why is the most valuable thing they can record. It
|
|
110
|
+
// directly identifies why the front door fails.
|
|
111
|
+
|
|
112
|
+
const BYPASSES_FILE = path.join(OUTCOMES_DIR, 'bypasses.json');
|
|
113
|
+
|
|
114
|
+
const BYPASS_REASONS = [
|
|
115
|
+
'forgot',
|
|
116
|
+
'faster',
|
|
117
|
+
'needed-editing',
|
|
118
|
+
'needed-context',
|
|
119
|
+
'model-quality',
|
|
120
|
+
'phewsh-in-the-way',
|
|
121
|
+
'other',
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
function loadBypasses() {
|
|
125
|
+
try { return JSON.parse(fs.readFileSync(BYPASSES_FILE, 'utf-8')); } catch { return []; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function recordBypass(reason, note = '') {
|
|
129
|
+
if (!BYPASS_REASONS.includes(reason)) {
|
|
130
|
+
throw new Error(`Reason must be one of: ${BYPASS_REASONS.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
const bypasses = loadBypasses();
|
|
133
|
+
bypasses.push({
|
|
134
|
+
ts: new Date().toISOString(),
|
|
135
|
+
project: path.basename(process.cwd()),
|
|
136
|
+
reason,
|
|
137
|
+
note: note.slice(0, 200),
|
|
138
|
+
});
|
|
139
|
+
fs.mkdirSync(OUTCOMES_DIR, { recursive: true });
|
|
140
|
+
fs.writeFileSync(BYPASSES_FILE, JSON.stringify(bypasses, null, 2));
|
|
141
|
+
return bypasses.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function bypassStats() {
|
|
145
|
+
const bypasses = loadBypasses();
|
|
146
|
+
const byReason = {};
|
|
147
|
+
for (const b of bypasses) byReason[b.reason] = (byReason[b.reason] || 0) + 1;
|
|
148
|
+
return {
|
|
149
|
+
total: bypasses.length,
|
|
150
|
+
byReason,
|
|
151
|
+
recent: bypasses.slice(-8).reverse(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
OUTCOMES, DECISIONS_FILE, BYPASS_REASONS, BYPASSES_FILE,
|
|
157
|
+
recordDecision, labelOutcome, pendingDecisions, recentDecisions, outcomeStats,
|
|
158
|
+
recordBypass, bypassStats,
|
|
159
|
+
};
|