phewsh 0.12.3 → 0.13.0

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 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 # Interactive AI session with .intent/ context
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 — create, plan, and work')}`);
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
+ };
@@ -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
- // ── First-run welcome ──────────────────────────────────
214
- if (!config?.apiKey) {
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(MODELS[currentModel].name));
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 (!config?.apiKey) {
257
- console.log(` ${cream('/key')} ${sage('to start ·')} ${cream('/tour')} ${sage('to explore ·')} ${cream('/help')} ${sage('for more')}`);
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(` ${ui.randomTip()}`);
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
- ['Model', MODELS[currentModel].name],
392
- ['Provider', MODELS[currentModel].provider],
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 model = MODELS[currentModel];
683
- ui.statusPanel('Provider', [
684
- ['API', 'Anthropic (direct)'],
685
- ['Model', model.name, 'cyan'],
686
- ['Endpoint', 'api.anthropic.com/v1/messages'],
687
- ['Key', config?.apiKey ? config.apiKey.slice(0, 8) + '...' : 'not set', config?.apiKey ? 'green' : 'yellow'],
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. Run /key to set one.')}`);
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 → send to AI
807
- if (!config?.apiKey) {
1066
+ // Regular input → route it (harness = your subscription, api = your key)
1067
+ if (!route) {
808
1068
  console.log('');
809
- console.log(` ${sage('Type')} ${cream('/key')} ${sage('to set your API key first.')}`);
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, systemPrompt, MODELS[currentModel].id);
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,115 @@
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
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
60
+
61
+ // ── 2. Pick the default route ─────────────────────────
62
+ const options = installed.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} — your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
63
+ options.push({ kind: 'api', id: 'api', label: 'Direct API — bring your own Anthropic/OpenRouter key' });
64
+
65
+ console.log(` ${b(cream('Where should phewsh route your work by default?'))}`);
66
+ options.forEach((o, i) => {
67
+ const current = (config.defaultRoute === o.id) ? ` ${teal('● current')}` : '';
68
+ console.log(` ${teal(String(i + 1))} ${sage(o.label)}${current}`);
69
+ });
70
+ console.log('');
71
+
72
+ const answer = await ask(rl, ` ${teal('>')} ${slate(`1-${options.length}, enter = ${options[0] ? '1' : 'skip'}: `)}`);
73
+ const idx = answer === '' ? 0 : parseInt(answer, 10) - 1;
74
+ const choice = options[idx];
75
+
76
+ if (!choice) {
77
+ console.log(` ${slate('Skipped — phewsh will auto-detect each session.')}`);
78
+ } else if (choice.kind === 'harness') {
79
+ config.defaultRoute = choice.id;
80
+ saveConfig(config);
81
+ console.log(` ${teal('●')} ${sage('Default route:')} ${cream(HARNESSES[choice.id].label)} ${slate('— no API key needed')}`);
82
+ } else {
83
+ // ── 3. API key, only if they chose the API route ────
84
+ config.defaultRoute = 'api';
85
+ if (config.apiKey) {
86
+ console.log(` ${teal('●')} ${sage('Default route: API — using your existing key')} ${slate('(' + config.apiKey.slice(0, 8) + '...)')}`);
87
+ saveConfig(config);
88
+ } else {
89
+ console.log('');
90
+ console.log(` ${sage('Anthropic:')} ${cream('console.anthropic.com/settings/keys')} ${slate('(sk-ant-...)')}`);
91
+ console.log(` ${sage('OpenRouter:')} ${cream('openrouter.ai/keys')} ${slate('(sk-or-...)')}`);
92
+ const key = await ask(rl, ` ${sage('Paste your API key (enter to skip):')}\n ${teal('>')} `);
93
+ if (key) {
94
+ config.apiKey = key;
95
+ config.provider = key.startsWith('sk-or-') ? 'openrouter' : 'anthropic';
96
+ console.log(` ${teal('●')} ${sage('Key saved.')}`);
97
+ } else {
98
+ delete config.defaultRoute;
99
+ console.log(` ${slate('No key — phewsh will fall back to any installed agent CLI.')}`);
100
+ }
101
+ saveConfig(config);
102
+ }
103
+ }
104
+
105
+ rl.close();
106
+
107
+ // ── 4. Done ───────────────────────────────────────────
108
+ console.log('');
109
+ ui.divider('line');
110
+ console.log(` ${teal('●')} ${b(cream('Setup complete.'))}`);
111
+ console.log('');
112
+ console.log(` ${sage('Start working:')} ${cream('phewsh')}`);
113
+ console.log(` ${slate('Optional: phewsh login (cloud sync) · phewsh intent --init (.intent/ for a project)')}`);
114
+ console.log('');
115
+ };
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}`)));
@@ -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
+ };
package/lib/supabase.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Supabase REST client for the CLI — no SDK, just fetch (Node 18+ built-in)
2
2
 
3
3
  const SUPABASE_URL = 'https://fpnpfnahwaztdlxuayyv.supabase.co';
4
- const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZwbnBmbmFod2F6dGRseHVheXl2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA1NDY2NTcsImV4cCI6MjA3NjEyMjY1N30.Q6mn8RIvXujBXbd10aFkeY7yGHVsAQPEHM5OzoPMsFQ';
4
+ const SUPABASE_ANON_KEY = 'sb_publishable_sL3R5aB43Yo5Ct0NQwB4fg_je9ccSHY';
5
5
 
6
6
  async function req(path, options = {}, accessToken = null) {
7
7
  const headers = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"