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 +10 -0
- package/commands/brief.js +13 -0
- package/commands/outcomes.js +15 -2
- package/commands/session.js +242 -73
- package/commands/setup.js +3 -2
- package/commands/truth.js +13 -0
- package/lib/brief.js +81 -0
- package/lib/harnesses.js +33 -15
- package/lib/intent-context.js +147 -0
- package/lib/intro.js +6 -2
- package/lib/lifecycle.js +283 -0
- package/lib/outcomes.js +31 -8
- package/lib/receipts-data.js +12 -7
- package/lib/route-coach.js +91 -0
- package/lib/selfheal.js +22 -5
- package/lib/sequencer/parsers/intent.js +4 -2
- package/lib/source-contract.js +79 -0
- package/lib/suggest.js +3 -2
- package/lib/truth.js +386 -0
- package/lib/ui.js +10 -1
- package/package.json +1 -1
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
|
+
});
|
package/commands/outcomes.js
CHANGED
|
@@ -170,8 +170,21 @@ function labelInteractive() {
|
|
|
170
170
|
}
|
|
171
171
|
const idx = parseInt(a, 10);
|
|
172
172
|
if (idx >= 1 && idx <= 4) {
|
|
173
|
-
|
|
174
|
-
|
|
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();
|
package/commands/session.js
CHANGED
|
@@ -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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
-
// /
|
|
998
|
-
if (
|
|
999
|
-
const
|
|
1000
|
-
|
|
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 =
|
|
1091
|
+
const r = applyReconciliation(proposal);
|
|
1003
1092
|
if (r.written) {
|
|
1004
|
-
|
|
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('
|
|
1099
|
+
console.log(` ${ember('!')} ${sage('Reconciliation was not applied: ' + r.reason)}`);
|
|
1007
1100
|
}
|
|
1008
1101
|
} else {
|
|
1009
|
-
console.log(` ${slate('
|
|
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
|
-
//
|
|
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
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
1286
|
-
console.log(
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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('/
|
|
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('
|
|
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 {
|
|
1528
|
-
const args =
|
|
1529
|
-
|
|
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(
|
|
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
|
-
//
|
|
2236
|
-
//
|
|
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(` ${
|
|
2259
|
-
|
|
2260
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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('
|
|
2345
|
-
console.log(` ${
|
|
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
|
-
|
|
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
|
+
});
|