phewsh 0.15.29 → 0.15.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/phewsh.js CHANGED
@@ -66,6 +66,8 @@ const COMMANDS = {
66
66
  mcp: () => require('../commands/mcp')(),
67
67
  receipts: () => require('../commands/receipts')(),
68
68
  outcomes: () => require('../commands/outcomes')(),
69
+ truth: () => require('../commands/truth'),
70
+ brief: () => require('../commands/brief'),
69
71
  bypass: () => require('../commands/bypass')(),
70
72
  setup: () => require('../commands/setup')(),
71
73
  update: () => require('../commands/update')(),
@@ -99,6 +101,8 @@ function showHelp() {
99
101
  console.log(` ${cyan('intent')} ${g('Create, view, evolve .intent/ artifacts')}`);
100
102
  console.log(` ${cyan('gate')} ${g('Set constraints (budget, time, skill, urgency)')}`);
101
103
  console.log(` ${cyan('context')} ${g('Export .intent/ for any AI tool')}`);
104
+ console.log(` ${cyan('truth')} ${g('Read-only audit: versions, Git, intent, projections, conflicts')}`);
105
+ console.log(` ${cyan('brief')} ${g('Provider-ready briefing built from verified project truth')}`);
102
106
  console.log(` ${cyan('ai')} ${g('One-shot prompt with .intent/ context')}
103
107
  ${cyan('browse')} ${g('Read any URL — AI summary in your terminal')}`);
104
108
  console.log('');
@@ -185,6 +189,12 @@ if (!command) {
185
189
  exitAfterUpdate(0);
186
190
  } else if (COMMANDS[command]) {
187
191
  COMMANDS[command]();
192
+ } else if (require('../lib/harnesses').HARNESSES[command]) {
193
+ // `phewsh <harness>` — a doorway shortcut. Launch the session and have it
194
+ // auto-run /work for that harness (preflight → brief → native handoff →
195
+ // postflight). This is what the phewsh.com doorways copy as a single command.
196
+ process.env.PHEWSH_AUTOWORK = command;
197
+ maybeFirstRunIntro().then(() => COMMANDS.session());
188
198
  } else {
189
199
  console.error(`\n Unknown command: ${command}\n Run 'phewsh help' for available commands.\n`);
190
200
  process.exit(1);
@@ -0,0 +1,13 @@
1
+ const { generateBrief } = require('../lib/brief');
2
+
3
+ async function main() {
4
+ const { content } = await generateBrief();
5
+ console.log(`\n${content}\n`);
6
+ }
7
+
8
+ module.exports = { main };
9
+
10
+ main().catch(err => {
11
+ console.error(`\nBrief unavailable: ${err.message}\n`);
12
+ process.exitCode = 1;
13
+ });
@@ -170,8 +170,21 @@ function labelInteractive() {
170
170
  }
171
171
  const idx = parseInt(a, 10);
172
172
  if (idx >= 1 && idx <= 4) {
173
- labelOutcome(d.id, OUTCOMES[idx - 1]);
174
- console.log(` ${teal('●')} ${OUTCOME_COLOR[OUTCOMES[idx - 1]](OUTCOMES[idx - 1])}`);
173
+ const outcome = OUTCOMES[idx - 1];
174
+ // A miss is only useful if phewsh learns WHY — that one line is what
175
+ // /recall surfaces before you repeat it. Match the inline flow.
176
+ if (outcome === 'reverted' || outcome === 'failed') {
177
+ console.log(` ${teal('●')} ${OUTCOME_COLOR[outcome](outcome)}`);
178
+ rl.question(` ${slate('why? (one line — Enter to skip)')} ${teal('>')} `, (why) => {
179
+ labelOutcome(d.id, outcome, why.trim() || null);
180
+ if (why.trim()) console.log(` ${slate('noted — /recall will remember why.')}`);
181
+ i++;
182
+ next();
183
+ });
184
+ return;
185
+ }
186
+ labelOutcome(d.id, outcome);
187
+ console.log(` ${teal('●')} ${OUTCOME_COLOR[outcome](outcome)}`);
175
188
  }
176
189
  i++;
177
190
  next();
@@ -17,17 +17,30 @@ const intentDir = () => path.join(process.cwd(), '.intent');
17
17
  const { select, refreshSession: refreshSess } = require('../lib/supabase');
18
18
  const { readPPS } = require('../lib/pps');
19
19
  const { push, pull, ensureValidToken } = require('./sync');
20
- const { HARNESSES, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
20
+ const { HARNESSES, interactiveLaunchArgs, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
21
21
  const { recordDecision, labelOutcome, pendingDecisions, recentDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
22
22
  const { suggest, suggestAll } = require('../lib/suggest');
23
23
  const continuity = require('../lib/continuity');
24
24
  const selfheal = require('../lib/selfheal');
25
25
  const learning = require('../lib/learning');
26
26
  const recall = require('../lib/recall');
27
+ const { routeCoach } = require('../lib/route-coach');
27
28
  const { closest } = require('../lib/closest');
28
29
  const cmdHistory = require('../lib/history');
29
30
  const { recordSessionEvent } = require('../lib/receipts-data');
30
31
  const configFile = require('../lib/config-file');
32
+ const { loadIntentContext } = require('../lib/intent-context');
33
+ const { auditTruth, formatTruth, quickVerifiedState } = require('../lib/truth');
34
+ const { generateBrief, persistBrief } = require('../lib/brief');
35
+ const {
36
+ applyReconciliation,
37
+ captureSnapshot,
38
+ createPostflight,
39
+ formatObservedReport,
40
+ observeCurrent,
41
+ reconciliationProposal,
42
+ } = require('../lib/lifecycle');
43
+ const { formatSourceContract } = require('../lib/source-contract');
31
44
  const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
32
45
  const {
33
46
  echoedRows,
@@ -168,24 +181,42 @@ const INTENT_MODES = {
168
181
  4: { id: 'review', label: 'Review', hint: 'The user wants critical review. Find what is wrong or risky before praising anything. Be specific about severity.' },
169
182
  };
170
183
 
184
+ const BARE_COMMANDS = {
185
+ help: '/help',
186
+ h: '/help',
187
+ next: '/next',
188
+ guide: '/next',
189
+ recommend: '/next',
190
+ tools: '/harnesses',
191
+ tool: '/harnesses',
192
+ agents: '/harnesses',
193
+ harnesses: '/harnesses',
194
+ routes: '/provider',
195
+ route: '/provider',
196
+ provider: '/provider',
197
+ status: '/status',
198
+ truth: '/truth',
199
+ brief: '/brief',
200
+ thread: '/thread',
201
+ outcomes: '/outcomes',
202
+ clarify: '/clarify',
203
+ setup: '/setup',
204
+ work: '/work',
205
+ quit: '/quit',
206
+ exit: '/quit',
207
+ };
208
+
171
209
  function loadConfig() {
172
210
  return configFile.loadConfig(CONFIG_PATH);
173
211
  }
174
212
 
175
213
  function saveConfig(config) {
176
- configFile.saveConfig(CONFIG_PATH, config);
177
- }
178
-
179
- function loadIntentContext() {
180
- const files = ['vision.md', 'plan.md', 'next.md'];
181
- const loaded = [];
182
- for (const file of files) {
183
- const p = path.join(intentDir(), file);
184
- if (fs.existsSync(p)) {
185
- loaded.push({ file, content: fs.readFileSync(p, 'utf-8') });
186
- }
214
+ try {
215
+ configFile.saveConfig(CONFIG_PATH, config);
216
+ return true;
217
+ } catch {
218
+ return false;
187
219
  }
188
- return loaded;
189
220
  }
190
221
 
191
222
  function buildSystemPrompt(intentFiles) {
@@ -195,8 +226,8 @@ function buildSystemPrompt(intentFiles) {
195
226
  return base + `\n\nNo .intent/ artifacts found in the current directory. The user hasn't set up project context yet — help them think through what they're building if they ask.`;
196
227
  }
197
228
 
198
- const sections = intentFiles.map(({ file, content }) =>
199
- `## ${file}\n\n${content.trim()}`
229
+ const sections = intentFiles.map(({ file, promptContent, content }) =>
230
+ `## ${file}\n\n${(promptContent || content).trim()}`
200
231
  ).join('\n\n---\n\n');
201
232
 
202
233
  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}`;
@@ -336,16 +367,41 @@ async function main() {
336
367
  let sessionMode = null; // INTENT_MODES id once picked
337
368
  let awaitingOutcome = null; // decision id eligible for 1-4 labeling
338
369
  let awaitingWhy = null; // { id, outcome } — next line is the reason
339
- let awaitingWrap = null; // { block } y/N to fold shipped work into next.md
370
+ let awaitingReconcile = null; // exact proposed append, applied only after y
371
+ let lastTransitionReport = null; // latest native preflight -> postflight comparison
340
372
  let awaitingFallback = null; // { input, fullSystem, options } after a route failure
341
373
  let bootstrapChoices = null; // root-bootstrap menu entries when no project here
342
374
  let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
343
375
  let pendingDidYouMean = null; // a suggested command; bare Enter accepts + runs it
344
376
  let decisionsThisSession = 0;
377
+ let lastRouteCoachId = null; // avoid repeating the same best-door hint
345
378
 
346
379
  // ── The Exhale: animated brand reveal ──────────────────
347
380
  await ui.brandReveal();
348
381
 
382
+ // Self-aware, plain-language summary — the value prop made real with THIS
383
+ // machine's state. Non-technical readers get one warm sentence; the rows
384
+ // below give engineers the exact facts. Fail-soft: a missing piece just
385
+ // shortens the line, it never blocks the door.
386
+ (function narrativeSummary() {
387
+ try {
388
+ const toolCount = harnesses.filter(h => h.installed).length;
389
+ const hasProject = intentFiles.length > 0;
390
+ const dot = teal('●');
391
+ const tools = `${toolCount} AI tool${toolCount !== 1 ? 's' : ''}`;
392
+ if (hasProject && toolCount > 0) {
393
+ console.log(` ${dot} ${sage(`${tools} here, all sharing one memory of`)} ${cream(projectName)}${sage(' — so you never start this project over.')}`);
394
+ } else if (hasProject) {
395
+ console.log(` ${dot} ${sage('phewsh holds one verified memory of')} ${cream(projectName)} ${sage('— ready for whatever AI tool you bring.')}`);
396
+ } else if (toolCount > 0) {
397
+ console.log(` ${dot} ${sage(`${tools} here.`)} ${sage('Run')} ${cream('/init')} ${sage('and they all work from one shared project memory.')}`);
398
+ } else {
399
+ console.log(` ${dot} ${sage('Install Claude Code, Codex, or Gemini — phewsh keeps them all on the same page.')}`);
400
+ }
401
+ console.log('');
402
+ } catch { /* the summary is a flourish, never a blocker */ }
403
+ })();
404
+
349
405
  if (config?.apiKey && !config.apiKey.startsWith('sk-')) {
350
406
  // Persist the cleanup so this never nags again — an unusable key
351
407
  // (not sk-…) helps nobody, and harness routes need no key at all.
@@ -437,6 +493,20 @@ async function main() {
437
493
  row('WEB', sage('local-only (works fine)') + slate(' · /login mirrors this at phewsh.com/intent'));
438
494
  }
439
495
 
496
+ // VERIFIED — the product thesis made visible: what's actually true in this
497
+ // repo right now, checked (not remembered). Fast + offline + fail-soft.
498
+ try {
499
+ const v = quickVerifiedState();
500
+ if (!v.available) {
501
+ row('VERIFIED', slate('not a git repo here — ') + sage('/truth') + slate(' audits whatever is present'));
502
+ } else {
503
+ const parts = [cream('HEAD ' + v.shortHead)];
504
+ parts.push(v.dirtyCount ? peach(`${v.dirtyCount} uncommitted`) : sage('clean'));
505
+ if (v.driftCommits > 0) parts.push(ember(`⚠ ${v.driftCommits} commit${v.driftCommits !== 1 ? 's' : ''} since .intent updated`));
506
+ row('VERIFIED', parts.join(slate(' · ')) + slate(' — /truth · /brief'));
507
+ }
508
+ } catch { /* the verified row is a glance, never a blocker */ }
509
+
440
510
  const oStats = outcomeStats();
441
511
  if (oStats.total > 0) {
442
512
  // Plain language: what phewsh has logged vs what you've taught it. The
@@ -543,13 +613,30 @@ async function main() {
543
613
  }
544
614
 
545
615
  // One subtle line under the menu — the single highest-leverage nudge, if any.
546
- function showInlineTip() {
616
+ function showInlineTip(filter = null) {
547
617
  let tip = null;
548
618
  try { tip = suggest(buildSuggestState()); } catch { /* never block the prompt on guidance */ }
549
619
  if (!tip) return;
620
+ if (filter && !filter(tip)) return;
550
621
  console.log(` ${teal('⤷')} ${sage(tip.message)} ${cream(tip.command.trim())} ${slate('· /next for options')}`);
551
622
  }
552
623
 
624
+ // The front door should steer, not trap. For a plain-language ask, point to
625
+ // the better native door when the pattern is obvious, then still run the turn.
626
+ function showRouteCoach(input) {
627
+ try {
628
+ const advice = routeCoach(input, {
629
+ route,
630
+ harnesses,
631
+ hasIntentDir: intentFiles.length > 0,
632
+ turnsThisSession: Math.floor(messages.length / 2),
633
+ });
634
+ if (!advice || advice.id === lastRouteCoachId) return;
635
+ lastRouteCoachId = advice.id;
636
+ console.log(` ${peach('↪')} ${sage(advice.message)} ${cream(advice.command)} ${slate('· still sending this turn')}`);
637
+ } catch { /* route coaching is advisory, never a blocker */ }
638
+ }
639
+
553
640
  // Open a known project from the bootstrap menu: chdir, reload memory,
554
641
  // back to the normal flow. The session is the cockpit; projects swap in.
555
642
  function openProjectAt(dir) {
@@ -649,6 +736,7 @@ async function main() {
649
736
  lastTurnFailure = null;
650
737
  awaitingOutcome = decisionId;
651
738
  console.log(slate(` via ${HARNESSES[harnessId].label} · how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing`));
739
+ showInlineTip((tip) => tip.id === 'capture-intent');
652
740
  return true;
653
741
  } catch (err) {
654
742
  turnInFlight = false;
@@ -699,6 +787,7 @@ async function main() {
699
787
  completionTokens: result.completionTokens,
700
788
  accessToken: config.supabaseAccessToken,
701
789
  });
790
+ showInlineTip((tip) => tip.id === 'capture-intent');
702
791
  return true;
703
792
  } catch (err) {
704
793
  turnInFlight = false;
@@ -816,10 +905,10 @@ async function main() {
816
905
  // so you know it registered. Arguments stay plain. TTY-only, fail-soft.
817
906
  const KNOWN_COMMANDS = new Set([
818
907
  'quit', 'exit', 'q', 'help', 'h', 'init', 'intent', 'clarify', 'model',
819
- 'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'run',
908
+ 'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'switch', 'run',
820
909
  'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
821
910
  'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
822
- 'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
911
+ 'agents', 'context', 'truth', 'brief', 'wrap', 'reconcile', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
823
912
  'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
824
913
  ]);
825
914
  const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
@@ -994,19 +1083,23 @@ async function main() {
994
1083
  return;
995
1084
  }
996
1085
 
997
- // /wrap confirmation: y folds the shipped-work block into next.md + heals.
998
- if (awaitingWrap) {
999
- const { block } = awaitingWrap;
1000
- awaitingWrap = null;
1086
+ // /reconcile confirmation: apply only the exact diff the user just saw.
1087
+ if (awaitingReconcile) {
1088
+ const proposal = awaitingReconcile;
1089
+ awaitingReconcile = null;
1001
1090
  if (/^(y|yes)$/i.test(input.trim())) {
1002
- const r = selfheal.appendToNext(block);
1091
+ const r = applyReconciliation(proposal);
1003
1092
  if (r.written) {
1004
- console.log(` ${green('✓')} ${sage('Folded into .intent/' + r.file + ' — CLAUDE.md synced too. The record matches the work now.')}`);
1093
+ const healed = selfheal.heal({ force: true });
1094
+ intentFiles = loadIntentContext();
1095
+ systemPrompt = buildSystemPrompt(intentFiles);
1096
+ console.log(` ${green('✓')} ${sage('Applied the approved diff to ' + r.target + '.')}`);
1097
+ console.log(` ${slate(healed.healed ? 'Generated Claude context refreshed from the approved intent change.' : 'Intent updated; generated context did not require a write.')}`);
1005
1098
  } else {
1006
- console.log(` ${ember('!')} ${sage('Could not write .intent/next.md left it untouched.')}`);
1099
+ console.log(` ${ember('!')} ${sage('Reconciliation was not applied: ' + r.reason)}`);
1007
1100
  }
1008
1101
  } else {
1009
- console.log(` ${slate('Left .intent/ as-is.')}`);
1102
+ console.log(` ${slate('No authoritative files were changed.')}`);
1010
1103
  }
1011
1104
  console.log('');
1012
1105
  rl.prompt();
@@ -1016,6 +1109,16 @@ async function main() {
1016
1109
  // Any typed input supersedes a pending "did you mean" offer.
1017
1110
  if (pendingDidYouMean) pendingDidYouMean = null;
1018
1111
 
1112
+ // Exact bare words are front-door controls too. Keep this exact-match only:
1113
+ // "help" opens help; "help me build X" still routes as natural language.
1114
+ if (!input.startsWith('/')) {
1115
+ const bare = input.trim().toLowerCase();
1116
+ if (BARE_COMMANDS[bare]) {
1117
+ await handleInput(BARE_COMMANDS[bare]);
1118
+ return;
1119
+ }
1120
+ }
1121
+
1019
1122
  // A bare number right after a route failure picks the fallback
1020
1123
  if (awaitingFallback) {
1021
1124
  const af = awaitingFallback;
@@ -1267,33 +1370,34 @@ async function main() {
1267
1370
  return;
1268
1371
  }
1269
1372
 
1270
- // ── /wrap ──────────────────────────────────────────
1271
- // Fold what you've shipped (git commits since .intent/ was last updated)
1272
- // back into next.md, then re-sync CLAUDE.md — so the record reflects the
1273
- // work, not last week. Deterministic from commit subjects; no LLM needed,
1274
- // so it works even with nothing connected. This is the cure for the drift
1275
- // that ate phewsh's own dogfood.
1373
+ // ── /wrap + /reconcile ─────────────────────────────
1374
+ // Wrap observes. Reconcile proposes. Only an explicit yes writes intent.
1276
1375
  if (cmd === 'wrap') {
1277
- const draft = selfheal.wrapDraft();
1278
1376
  console.log('');
1279
- if (!draft) {
1280
- console.log(` ${teal('●')} ${sage('Nothing to fold in — your .intent/ is already current with git (or this isn\'t a repo).')}`);
1377
+ const truth = await auditTruth();
1378
+ const observed = observeCurrent(truth);
1379
+ lastTransitionReport = observed;
1380
+ console.log(formatObservedReport(observed, { title: 'Wrap — observed current state' }));
1381
+ console.log('');
1382
+ rl.prompt();
1383
+ return;
1384
+ }
1385
+
1386
+ if (cmd === 'reconcile') {
1387
+ console.log('');
1388
+ const report = lastTransitionReport || observeCurrent(await auditTruth());
1389
+ const proposal = reconciliationProposal(report);
1390
+ if (!proposal.available) {
1391
+ console.log(` ${ember('!')} ${sage('No reconciliation proposal: ' + proposal.reason)}`);
1281
1392
  console.log('');
1282
1393
  rl.prompt();
1283
1394
  return;
1284
1395
  }
1285
- const n = draft.commits.length;
1286
- console.log(` ${b(cream('Wrap'))} ${slate('— ' + n + ' commit' + (n !== 1 ? 's' : '') + ' shipped since .intent/ was last updated')}`);
1287
- ui.divider('line');
1288
- for (const s of draft.commits.slice(0, 12)) {
1289
- let line = s.replace(/\s+/g, ' ');
1290
- if (line.length > 70) line = line.slice(0, 69).trimEnd() + '…';
1291
- console.log(` ${sage('+')} ${cream(line)}`);
1292
- }
1293
- if (n > 12) console.log(` ${slate('… and ' + (n - 12) + ' more')}`);
1294
- ui.divider('line');
1295
- console.log(` ${sage('Fold this into')} ${cream('.intent/next.md')} ${sage('and re-sync CLAUDE.md?')} ${slate('y/N')}`);
1296
- awaitingWrap = { block: draft.block };
1396
+ console.log('Reconciliation proposal (no files changed):');
1397
+ console.log(proposal.diff);
1398
+ console.log('');
1399
+ console.log(` ${sage('Apply this exact diff?')} ${slate('y/N')}`);
1400
+ awaitingReconcile = proposal;
1297
1401
  rl.prompt();
1298
1402
  return;
1299
1403
  }
@@ -1372,7 +1476,10 @@ async function main() {
1372
1476
  console.log(` ${cream('author .intent/')}`);
1373
1477
  console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
1374
1478
  console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
1375
- console.log(` ${teal('/wrap')} ${sage('Fold what you shipped (git) into .intent/ keeps the record current')}`);
1479
+ console.log(` ${teal('/truth')} ${sage('Read-only audit of versions, Git, intent, projections, and conflicts')}`);
1480
+ console.log(` ${teal('/brief')} ${sage('Generate the current provider-ready verified briefing')}`);
1481
+ console.log(` ${teal('/wrap')} ${sage('Observe changes, contradictions, unknowns, and reconciliation needs')}`);
1482
+ console.log(` ${teal('/reconcile')} ${sage('Propose an exact intent diff; writes only after approval')}`);
1376
1483
  console.log(` ${teal('/clarify')} ${sage('Turn ideas into .intent/ artifacts')}`);
1377
1484
  console.log(` ${teal('/gate')} ${sage('Set constraints (budget, time, skill)')}`);
1378
1485
  console.log(` ${teal('/context')} ${sage('Show loaded .intent/ files')}`);
@@ -1398,7 +1505,8 @@ async function main() {
1398
1505
  console.log(` ${teal('/outcomes')} ${sage('What worked — tell phewsh, and it learns which tool to trust')}`);
1399
1506
  console.log('');
1400
1507
  console.log(` ${cream('session')}`);
1401
- console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('Hand off to interactive Claude Code/Codex — outcome on return')}`);
1508
+ console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('Preflight, brief, native handoff, automatic postflight')}`);
1509
+ console.log(` ${teal('/switch')} ${slate('<harness>')} ${sage('Launch another native tool with a freshly verified briefing')}`);
1402
1510
  console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
1403
1511
  console.log(` ${teal('esc')} ${sage('Cancel a running turn · clear the input line')}`);
1404
1512
  console.log(` ${teal('/clear')} ${sage('Clear conversation')}`);
@@ -1454,6 +1562,7 @@ async function main() {
1454
1562
  ui.divider('line');
1455
1563
  intentFiles.forEach(f => console.log(` ${teal('●')} ${cream(f.file)} ${slate('(' + f.content.length + ' chars)')}`));
1456
1564
  ui.divider('line');
1565
+ console.log(formatSourceContract({ compact: true }).split('\n').map(line => ` ${line}`).join('\n'));
1457
1566
  } else {
1458
1567
  console.log(`\n ${sage('No .intent/ context found in')} ${slate(process.cwd())}`);
1459
1568
  console.log(` ${sage('Run')} ${cream('/init')} ${sage('to create one')}`);
@@ -1463,6 +1572,23 @@ async function main() {
1463
1572
  return;
1464
1573
  }
1465
1574
 
1575
+ if (cmd === 'truth') {
1576
+ console.log('');
1577
+ console.log(formatTruth(await auditTruth()));
1578
+ console.log('');
1579
+ rl.prompt();
1580
+ return;
1581
+ }
1582
+
1583
+ if (cmd === 'brief') {
1584
+ console.log('');
1585
+ const { content } = await generateBrief();
1586
+ console.log(content);
1587
+ console.log('');
1588
+ rl.prompt();
1589
+ return;
1590
+ }
1591
+
1466
1592
  if (cmd === 'status') {
1467
1593
  const turns = messages.length / 2;
1468
1594
  config = loadConfig();
@@ -1518,15 +1644,14 @@ async function main() {
1518
1644
  }
1519
1645
 
1520
1646
  if (cmd === 'clarify') {
1521
- if (!config?.apiKey) {
1522
- console.log(`\n ${ember('!')} ${sage('No API key. Run /key to set one.')}\n`);
1523
- rl.prompt();
1524
- return;
1525
- }
1526
1647
  try {
1527
- const { execSync } = require('child_process');
1528
- const args = cmdArg ? `--text "${cmdArg.replace(/"/g, '\\"')}"` : '';
1529
- execSync(`node ${path.join(__dirname, 'clarify.js')} ${args}`, { stdio: 'inherit' });
1648
+ const { spawnSync } = require('child_process');
1649
+ const args = ['clarify'];
1650
+ if (cmdArg) args.push('--text', cmdArg);
1651
+ const res = spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), ...args], { stdio: 'inherit' });
1652
+ if (res.status !== 0) {
1653
+ console.error(` ${ember('!')} ${sage('Clarify exited without writing context.')}`);
1654
+ }
1530
1655
  intentFiles = loadIntentContext();
1531
1656
  systemPrompt = buildSystemPrompt(intentFiles);
1532
1657
  if (intentFiles.length > 0) {
@@ -2045,7 +2170,7 @@ async function main() {
2045
2170
  }
2046
2171
  if (!h.installed) continue; // hide the long tail of uninstalled extras
2047
2172
  const via = h.headless ? '' : ' · /work only';
2048
- rows.push([h.label, `ready — ${h.role}${via}`, 'green']);
2173
+ rows.push([h.label, `ready — ${h.bestFor || h.role}${via}`, 'green']);
2049
2174
  }
2050
2175
  rows.push(['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '... (' + (config.provider || 'anthropic') + ')' : 'not set — optional', config?.apiKey ? 'green' : 'yellow']);
2051
2176
  rows.push(['Fallback', (config?.fallback === 'auto' ? 'auto-switch on failure' : 'ask before switching') + ' — /fallback to change', 'peach']);
@@ -2137,7 +2262,7 @@ async function main() {
2137
2262
  const mode = h.headless ? '' : slate(' · /work only');
2138
2263
  const badge = hStats ? learning.keptBadge(hStats, h.id) : '';
2139
2264
  const rec = badge ? ` ${slate('· ' + badge)}` : '';
2140
- console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.role || h.label).padEnd(20))} ${slate(h.label)}${mode}${rec}${active}`);
2265
+ console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.bestFor || h.role || h.label).padEnd(38))} ${slate(h.label)}${mode}${rec}${active}`);
2141
2266
  }
2142
2267
  ui.divider('line');
2143
2268
  const learned = hStats ? learning.learningLine(hStats) : null;
@@ -2231,10 +2356,9 @@ async function main() {
2231
2356
  return;
2232
2357
  }
2233
2358
 
2234
- if (cmd === 'work') {
2235
- // Real work needs the real harness. Hand the terminal over to an
2236
- // interactive session, take it back when they exit, ask the outcome.
2237
- // phewsh stays the front door AND the return point.
2359
+ if (cmd === 'work' || cmd === 'switch') {
2360
+ // Native handoff lifecycle: verify -> brief -> release the terminal ->
2361
+ // observe -> reconcile. The harness owns its UI while it runs.
2238
2362
  const target = cmdArg?.trim().toLowerCase() || (route?.type === 'harness' ? route.id : 'claude-code');
2239
2363
  const h = HARNESSES[target];
2240
2364
  if (!h) {
@@ -2247,6 +2371,13 @@ async function main() {
2247
2371
  rl.prompt();
2248
2372
  return;
2249
2373
  }
2374
+
2375
+ const preflightTruth = await auditTruth();
2376
+ const before = captureSnapshot(preflightTruth);
2377
+ const generatedBrief = await generateBrief({ report: preflightTruth });
2378
+ const launchBrief = `${generatedBrief.content}\n\nThis is a PHEWSH transition briefing. Use it as project context, preserve native tool behavior, and verify claims against the repository before acting. When you finish, exit this tool — PHEWSH resumes, runs an automatic postflight comparing what changed against this brief, and offers reconciliation.`;
2379
+ const savedBrief = persistBrief(generatedBrief.content, { project: projectName, route: target });
2380
+ const launch = interactiveLaunchArgs(target, launchBrief, { model: harnessModel });
2250
2381
  const decisionId = recordDecision({
2251
2382
  project: projectName,
2252
2383
  route: target,
@@ -2255,23 +2386,49 @@ async function main() {
2255
2386
  });
2256
2387
  decisionsThisSession++;
2257
2388
  console.log('');
2258
- console.log(` ${teal('●')} ${sage('Handing the terminal to')} ${cream(h.label)} ${slate('— exit to come back to phewsh')}`);
2259
- if (fs.existsSync(path.join(process.cwd(), 'CLAUDE.md')) || intentFiles.length > 0) {
2260
- console.log(` ${slate('your .intent/ context rides along via CLAUDE.md')}`);
2261
- }
2389
+ console.log(` ${b(cream('Work preflight'))} ${slate('— verified before native handoff')}`);
2390
+ ui.divider('line');
2391
+ console.log(` ${sage('Route:')} ${cream(h.label)}${harnessModel ? slate(' · model ' + harnessModel) : slate(' · native default model')}`);
2392
+ console.log(` ${sage('Git:')} ${cream(preflightTruth.git.shortHead || 'unknown')} ${slate('· ' + before.dirty.length + ' uncommitted path(s) before work')}`);
2393
+ console.log(` ${sage('Truth:')} ${preflightTruth.conflicts.length ? peach(preflightTruth.conflicts.length + ' conflict(s) carried explicitly') : green('no explicit conflicts')}`);
2394
+ console.log(` ${sage('Brief:')} ${launch.briefingPassed ? green('passed directly to the native harness') : peach('not injectable for this harness; available via /brief')}`);
2395
+ console.log(` ${sage('Record:')} ${savedBrief.written ? slate('exact briefing saved locally for this transition') : peach('briefing persistence unavailable; launch continues')}`);
2396
+ console.log(` ${slate('After exit PHEWSH will compare Git, files, intent claims, generated drift, and contradictions.')}`);
2397
+ ui.divider('line');
2398
+ console.log(` ${teal('●')} ${sage('Handing the terminal to')} ${cream(h.label)} ${slate('— exit to return for postflight')}`);
2262
2399
  console.log('');
2400
+ recordSessionEvent(target, projectName, 'work_started', {
2401
+ taskId: decisionId,
2402
+ summary: `interactive ${h.label} session`,
2403
+ gitHead: before.head,
2404
+ dirtyPaths: before.dirty,
2405
+ briefingHash: savedBrief.hash,
2406
+ briefingFile: savedBrief.file,
2407
+ briefingPassed: launch.briefingPassed,
2408
+ });
2263
2409
  pasteMode(false);
2264
2410
  rl.pause();
2265
2411
  const { spawnSync } = require('child_process');
2266
- const res = spawnSync(h.bin, [], { stdio: 'inherit' });
2412
+ const res = spawnSync(h.bin, launch.args, { stdio: 'inherit' });
2267
2413
  rl.resume();
2268
2414
  pasteMode(true);
2415
+ const postflight = await createPostflight(before);
2416
+ lastTransitionReport = postflight;
2269
2417
  recordSessionEvent(target, projectName, 'task_complete', {
2270
- taskId: decisionId, success: res.status === 0, summary: `interactive ${h.label} session`,
2418
+ taskId: decisionId,
2419
+ success: res.status === 0,
2420
+ summary: `interactive ${h.label} session`,
2421
+ gitHeadBefore: before.head,
2422
+ gitHeadAfter: postflight.afterHead,
2423
+ changedFiles: postflight.files,
2424
+ conflicts: postflight.conflicts,
2425
+ briefingHash: savedBrief.hash,
2271
2426
  });
2272
2427
  awaitingOutcome = decisionId;
2273
2428
  console.log('');
2274
- console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate("how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing")}`);
2429
+ console.log(formatObservedReport(postflight, { title: `${h.label} session ended postflight` }));
2430
+ console.log('');
2431
+ console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate("1 kept · 2 undid · 3 redid · 4 flopped · /reconcile · /switch <tool>")}`);
2275
2432
  console.log('');
2276
2433
  rl.prompt();
2277
2434
  return;
@@ -2341,8 +2498,10 @@ async function main() {
2341
2498
  // Regular input → route it (harness = your subscription, api = your key)
2342
2499
  if (!route) {
2343
2500
  console.log('');
2344
- console.log(` ${sage('No route yet')} ${cream('/key')} ${sage('to set an API key, or install Claude Code / Codex')}`);
2345
- console.log(` ${slate('phewsh uses installed agent CLIs automatically, no key needed.')}`);
2501
+ console.log(` ${sage('That request is valid phewsh just needs an AI worker behind the door.')}`);
2502
+ console.log(` ${cream('phewsh setup')} ${sage('detects installed tools and picks a route')}`);
2503
+ console.log(` ${cream('/key')} ${sage('adds an API key, or install Claude Code / Codex / Gemini and rerun phewsh')}`);
2504
+ console.log(` ${slate('Once a route exists, plain typing works; no slash command needed.')}`);
2346
2505
  console.log('');
2347
2506
  rl.prompt();
2348
2507
  return;
@@ -2373,6 +2532,8 @@ async function main() {
2373
2532
  return;
2374
2533
  }
2375
2534
 
2535
+ showRouteCoach(input);
2536
+
2376
2537
  const ok = route.type === 'harness'
2377
2538
  ? await runHarnessTurn(input, route.id, fullSystem)
2378
2539
  : await runApiTurn(input, fullSystem);
@@ -2411,6 +2572,14 @@ async function main() {
2411
2572
  console.log(`\n ${sage('session ended')}\n`);
2412
2573
  process.exit(0);
2413
2574
  });
2575
+
2576
+ // Doorway shortcut: `phewsh <harness>` sets PHEWSH_AUTOWORK so we drop the
2577
+ // user straight into /work for that tool after the front door renders.
2578
+ const autoWork = process.env.PHEWSH_AUTOWORK;
2579
+ if (autoWork && HARNESSES[autoWork]) {
2580
+ delete process.env.PHEWSH_AUTOWORK;
2581
+ handleInput('/work ' + autoWork).catch(() => {});
2582
+ }
2414
2583
  }
2415
2584
 
2416
2585
  main().catch(err => {
package/commands/setup.js CHANGED
@@ -47,7 +47,8 @@ module.exports = async function setup() {
47
47
  for (const h of harnesses) {
48
48
  const status = h.installed ? green('✓ installed') : slate('✗ not installed');
49
49
  const mode = h.headless ? '' : slate(' · interactive — /work ' + h.id);
50
- console.log(` ${cream(h.label.padEnd(14))} ${status} ${slate('(' + h.auth + ')')}${mode}`);
50
+ const fit = h.bestFor ? slate(' · best for ' + h.bestFor) : '';
51
+ console.log(` ${cream(h.label.padEnd(14))} ${status} ${slate('(' + h.auth + ')')}${mode}${fit}`);
51
52
  }
52
53
  if (installed.length === 0) {
53
54
  console.log('');
@@ -95,7 +96,7 @@ module.exports = async function setup() {
95
96
  // ── 2. Pick the default route ─────────────────────────
96
97
  // Interactive-only harnesses (Hermes, Pi) can't take chat routing — they
97
98
  // stay reachable via /work in a session, so they're not default options.
98
- const options = chatCapable.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} — your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
99
+ const options = chatCapable.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} — ${h.bestFor || h.role}; your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
99
100
  options.push({ kind: 'api', id: 'api', label: 'Direct API — bring your own Anthropic/OpenRouter key' });
100
101
 
101
102
  console.log(` ${b(cream('Where should phewsh route your work by default?'))}`);
@@ -0,0 +1,13 @@
1
+ const { auditTruth, formatTruth } = require('../lib/truth');
2
+
3
+ async function main() {
4
+ const report = await auditTruth();
5
+ console.log(`\n${formatTruth(report)}\n`);
6
+ }
7
+
8
+ module.exports = { main };
9
+
10
+ main().catch(err => {
11
+ console.error(`\nTruth audit unavailable: ${err.message}\n`);
12
+ process.exitCode = 1;
13
+ });