phewsh 0.15.28 → 0.15.30
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 +4 -0
- package/commands/brief.js +13 -0
- package/commands/outcomes.js +91 -44
- package/commands/session.js +156 -73
- package/commands/truth.js +13 -0
- package/lib/brief.js +81 -0
- package/lib/harnesses.js +23 -5
- package/lib/intent-context.js +147 -0
- package/lib/lifecycle.js +283 -0
- package/lib/outcomes.js +106 -15
- package/lib/receipts-data.js +12 -7
- 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 +4 -2
- package/lib/truth.js +386 -0
- 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('');
|
|
@@ -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
|
@@ -10,6 +10,8 @@ const {
|
|
|
10
10
|
OUTCOMES, recordDecision, labelOutcome,
|
|
11
11
|
pendingDecisions, recentDecisions, outcomeStats, bypassStats,
|
|
12
12
|
} = require('../lib/outcomes');
|
|
13
|
+
const learning = require('../lib/learning');
|
|
14
|
+
const continuity = require('../lib/continuity');
|
|
13
15
|
|
|
14
16
|
const { b, teal, peach, sage, slate, cream, ember, green } = ui;
|
|
15
17
|
|
|
@@ -42,76 +44,108 @@ function showBypasses() {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// Plain-language explanation of the contract — shown every time, because the
|
|
48
|
+
// #1 complaint was "I have no idea what this is for or how to use it."
|
|
49
|
+
function explainDeal() {
|
|
50
|
+
console.log(` ${b(cream('Outcomes'))} ${slate('— phewsh\'s memory of what actually worked')}`);
|
|
51
|
+
ui.divider('line');
|
|
52
|
+
console.log(` ${sage('Every call you route, phewsh logs. Tell it what you')} ${green('kept')} ${sage('vs')} ${ember('threw out')}${sage(',')}`);
|
|
53
|
+
console.log(` ${sage('and it learns which tool to trust for which job — and warns you before')}`);
|
|
54
|
+
console.log(` ${sage('you repeat something that flopped.')} ${slate('That labeled record is the one thing')}`);
|
|
55
|
+
console.log(` ${slate('your chat history doesn\'t have. Two taps now = smarter routing later.')}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The payoff, in their own data — so labeling feels like it buys something.
|
|
59
|
+
function showPayoff(stats) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
try {
|
|
62
|
+
const best = learning.bestRoute(stats, { minSample: 2 });
|
|
63
|
+
if (best && best.keptRate >= 0.5) {
|
|
64
|
+
const label = continuity.labelFor(best.route) || best.route;
|
|
65
|
+
lines.push(`${green('●')} ${sage(label + ' keeps best for you — ')}${cream(Math.round(best.keptRate * 100) + '%')} ${sage(`(${best.kept}/${best.total}). phewsh leans there when it routes.`)}`);
|
|
66
|
+
}
|
|
67
|
+
} catch { /* best-effort */ }
|
|
68
|
+
const regrets = stats.reverted + stats.failed;
|
|
69
|
+
if (regrets > 0) {
|
|
70
|
+
lines.push(`${ember('●')} ${sage((regrets === 1 ? '1 call you marked a miss is' : regrets + ' calls you marked misses are') + ' remembered — ')}${cream('/recall')} ${sage('warns you before a repeat.')}`);
|
|
71
|
+
}
|
|
72
|
+
if (lines.length === 0) return;
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log(` ${b(cream('What it already knows'))} ${slate('← this is the payoff')}`);
|
|
75
|
+
for (const l of lines) console.log(` ${l}`);
|
|
76
|
+
if (stats.judged < 4) console.log(` ${slate('judge a few more and clearer patterns surface here.')}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
function showStats() {
|
|
46
80
|
const stats = outcomeStats();
|
|
47
|
-
const labeled = stats.total - stats.pending;
|
|
48
81
|
|
|
49
82
|
console.log('');
|
|
50
|
-
|
|
51
|
-
ui.divider('line');
|
|
83
|
+
explainDeal();
|
|
52
84
|
|
|
53
85
|
if (stats.total === 0) {
|
|
54
|
-
console.log(
|
|
55
|
-
console.log(` ${
|
|
56
|
-
console.log(` ${slate('
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(` ${sage('Nothing logged yet — start routing work and it fills in.')}`);
|
|
88
|
+
console.log(` ${slate('In a session: just type, or use /use <tool>. Then tap 1-4 to judge it.')}`);
|
|
57
89
|
showBypasses();
|
|
58
90
|
console.log('');
|
|
59
91
|
return;
|
|
60
92
|
}
|
|
61
93
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
// The state of the record, in plain numbers that don't lie.
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(` ${b(cream('Your record'))}`);
|
|
97
|
+
console.log(` ${cream(String(stats.total))} ${sage('routed')} ${slate('·')} ${cream(String(stats.judged))} ${sage('you\'ve judged')} ${slate('·')} ${cream(String(stats.pending))} ${sage('not yet judged')}`);
|
|
98
|
+
if (stats.judged > 0) {
|
|
99
|
+
const bits = [green(`✓ ${stats.kept} kept`)];
|
|
100
|
+
if (stats.reverted) bits.push(ember(`↩ ${stats.reverted} reverted`));
|
|
101
|
+
if (stats.superseded) bits.push(peach(`⟳ ${stats.superseded} redone`));
|
|
102
|
+
if (stats.failed) bits.push(ember(`✗ ${stats.failed} flopped`));
|
|
103
|
+
console.log(` ${bits.join(slate(' · '))}`);
|
|
65
104
|
}
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
}
|
|
105
|
+
if (stats.autoFailed > 0) {
|
|
106
|
+
console.log(` ${slate(`(${stats.autoFailed} didn't complete — route errors phewsh logged for you, not your call)`)}`);
|
|
75
107
|
}
|
|
76
108
|
|
|
77
|
-
|
|
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
|
-
}
|
|
109
|
+
showPayoff(stats);
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
// The ask — focused on recent, real calls, with a dead-simple next step.
|
|
112
|
+
const pending = pendingDecisions({ substantive: true }).slice(-6).reverse();
|
|
113
|
+
if (pending.length > 0) {
|
|
89
114
|
console.log('');
|
|
90
|
-
console.log(` ${b(cream('
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
console.log(` ${b(cream('Give a verdict'))} ${slate('— your recent calls (skip the trivial ones)')}`);
|
|
116
|
+
pending.forEach((d, i) => {
|
|
117
|
+
let s = (d.summary || '').replace(/\s+/g, ' ');
|
|
118
|
+
if (s.length > 44) s = s.slice(0, 43).trimEnd() + '…';
|
|
119
|
+
const via = (continuity.labelFor(d.route) || d.route).padEnd(12);
|
|
120
|
+
console.log(` ${teal(String(i + 1))} ${cream(s.padEnd(45))} ${slate(via)} ${slate(fmtAgo(d.ts))}`);
|
|
121
|
+
});
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(` ${teal('→')} ${sage('Walk through them:')} ${cream('phewsh outcomes label')} ${slate('· 20 seconds, Enter skips')}`);
|
|
124
|
+
console.log(` ${slate('or in a session, just tap')} ${cream('1-4')} ${slate('right after any answer.')}`);
|
|
94
125
|
}
|
|
95
126
|
|
|
96
127
|
showBypasses();
|
|
97
|
-
|
|
98
|
-
if (stats.pending > 0) {
|
|
99
|
-
console.log('');
|
|
100
|
-
console.log(` ${sage('Label pending decisions:')} ${cream('phewsh outcomes label')}`);
|
|
101
|
-
}
|
|
102
128
|
console.log('');
|
|
103
129
|
}
|
|
104
130
|
|
|
105
131
|
function labelInteractive() {
|
|
106
|
-
|
|
132
|
+
// Newest first, and capped — you judge best while the work is fresh, and a
|
|
133
|
+
// 76-item slog through last week's test prompts is exactly the chore that
|
|
134
|
+
// made this feel pointless. The recent calls carry the signal.
|
|
135
|
+
const CAP = 12;
|
|
136
|
+
const all = pendingDecisions({ substantive: true }).reverse(); // newest first
|
|
137
|
+
const pending = all.slice(0, CAP);
|
|
107
138
|
if (pending.length === 0) {
|
|
108
|
-
console.log(`\n ${teal('●')} ${sage('
|
|
139
|
+
console.log(`\n ${teal('●')} ${sage('Nothing substantive to judge — your real calls are all labeled.')}\n`);
|
|
109
140
|
return;
|
|
110
141
|
}
|
|
111
142
|
|
|
112
143
|
console.log('');
|
|
113
|
-
|
|
114
|
-
console.log(` ${
|
|
144
|
+
const more = all.length > CAP ? slate(` (most recent ${CAP}; ${all.length - CAP} older left alone)`) : '';
|
|
145
|
+
console.log(` ${b(cream(`Judging your recent call${pending.length !== 1 ? 's' : ''}`))}${more}`);
|
|
146
|
+
console.log(` ${slate('For each: what happened to it?')}`);
|
|
147
|
+
console.log(` ${green('1')} ${sage('kept it')} ${ember('2')} ${sage('undid it')} ${peach('3')} ${sage('redid it differently')} ${ember('4')} ${sage('it flopped')}`);
|
|
148
|
+
console.log(` ${slate('Enter = skip (judge later) · q = stop. This is what teaches phewsh.')}`);
|
|
115
149
|
ui.divider('line');
|
|
116
150
|
|
|
117
151
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -136,8 +170,21 @@ function labelInteractive() {
|
|
|
136
170
|
}
|
|
137
171
|
const idx = parseInt(a, 10);
|
|
138
172
|
if (idx >= 1 && idx <= 4) {
|
|
139
|
-
|
|
140
|
-
|
|
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)}`);
|
|
141
188
|
}
|
|
142
189
|
i++;
|
|
143
190
|
next();
|
package/commands/session.js
CHANGED
|
@@ -17,7 +17,7 @@ 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');
|
|
@@ -28,6 +28,18 @@ const { closest } = require('../lib/closest');
|
|
|
28
28
|
const cmdHistory = require('../lib/history');
|
|
29
29
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
30
30
|
const configFile = require('../lib/config-file');
|
|
31
|
+
const { loadIntentContext } = require('../lib/intent-context');
|
|
32
|
+
const { auditTruth, formatTruth, quickVerifiedState } = require('../lib/truth');
|
|
33
|
+
const { generateBrief, persistBrief } = require('../lib/brief');
|
|
34
|
+
const {
|
|
35
|
+
applyReconciliation,
|
|
36
|
+
captureSnapshot,
|
|
37
|
+
createPostflight,
|
|
38
|
+
formatObservedReport,
|
|
39
|
+
observeCurrent,
|
|
40
|
+
reconciliationProposal,
|
|
41
|
+
} = require('../lib/lifecycle');
|
|
42
|
+
const { formatSourceContract } = require('../lib/source-contract');
|
|
31
43
|
const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
|
|
32
44
|
const {
|
|
33
45
|
echoedRows,
|
|
@@ -173,19 +185,12 @@ function loadConfig() {
|
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
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
|
-
}
|
|
188
|
+
try {
|
|
189
|
+
configFile.saveConfig(CONFIG_PATH, config);
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
187
193
|
}
|
|
188
|
-
return loaded;
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
function buildSystemPrompt(intentFiles) {
|
|
@@ -195,8 +200,8 @@ function buildSystemPrompt(intentFiles) {
|
|
|
195
200
|
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
201
|
}
|
|
197
202
|
|
|
198
|
-
const sections = intentFiles.map(({ file, content }) =>
|
|
199
|
-
`## ${file}\n\n${content.trim()}`
|
|
203
|
+
const sections = intentFiles.map(({ file, promptContent, content }) =>
|
|
204
|
+
`## ${file}\n\n${(promptContent || content).trim()}`
|
|
200
205
|
).join('\n\n---\n\n');
|
|
201
206
|
|
|
202
207
|
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,7 +341,8 @@ async function main() {
|
|
|
336
341
|
let sessionMode = null; // INTENT_MODES id once picked
|
|
337
342
|
let awaitingOutcome = null; // decision id eligible for 1-4 labeling
|
|
338
343
|
let awaitingWhy = null; // { id, outcome } — next line is the reason
|
|
339
|
-
let
|
|
344
|
+
let awaitingReconcile = null; // exact proposed append, applied only after y
|
|
345
|
+
let lastTransitionReport = null; // latest native preflight -> postflight comparison
|
|
340
346
|
let awaitingFallback = null; // { input, fullSystem, options } after a route failure
|
|
341
347
|
let bootstrapChoices = null; // root-bootstrap menu entries when no project here
|
|
342
348
|
let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
|
|
@@ -437,14 +443,31 @@ async function main() {
|
|
|
437
443
|
row('WEB', sage('local-only (works fine)') + slate(' · /login mirrors this at phewsh.com/intent'));
|
|
438
444
|
}
|
|
439
445
|
|
|
446
|
+
// VERIFIED — the product thesis made visible: what's actually true in this
|
|
447
|
+
// repo right now, checked (not remembered). Fast + offline + fail-soft.
|
|
448
|
+
try {
|
|
449
|
+
const v = quickVerifiedState();
|
|
450
|
+
if (!v.available) {
|
|
451
|
+
row('VERIFIED', slate('not a git repo here — ') + sage('/truth') + slate(' audits whatever is present'));
|
|
452
|
+
} else {
|
|
453
|
+
const parts = [cream('HEAD ' + v.shortHead)];
|
|
454
|
+
parts.push(v.dirtyCount ? peach(`${v.dirtyCount} uncommitted`) : sage('clean'));
|
|
455
|
+
if (v.driftCommits > 0) parts.push(ember(`⚠ ${v.driftCommits} commit${v.driftCommits !== 1 ? 's' : ''} since .intent updated`));
|
|
456
|
+
row('VERIFIED', parts.join(slate(' · ')) + slate(' — /truth · /brief'));
|
|
457
|
+
}
|
|
458
|
+
} catch { /* the verified row is a glance, never a blocker */ }
|
|
459
|
+
|
|
440
460
|
const oStats = outcomeStats();
|
|
441
|
-
const oLabeled = oStats.total - oStats.pending;
|
|
442
461
|
if (oStats.total > 0) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
462
|
+
// Plain language: what phewsh has logged vs what you've taught it. The
|
|
463
|
+
// kept-rate counts only the calls you actually judged (auto route-errors
|
|
464
|
+
// don't drag it down) — see /outcomes for the full picture + payoff.
|
|
465
|
+
const tail = oStats.judged > 0
|
|
466
|
+
? slate(' · ') + sage(`${Math.round((oStats.kept / oStats.judged) * 100)}% of what you judged, you kept`)
|
|
467
|
+
: slate(' · ') + peach('none judged yet — tell it what you kept');
|
|
468
|
+
row('RECORD', cream(`${oStats.total} routed`) + tail + slate(' — /outcomes'));
|
|
446
469
|
} else {
|
|
447
|
-
row('RECORD', slate('empty —
|
|
470
|
+
row('RECORD', slate('empty — phewsh starts logging what you route as you work'));
|
|
448
471
|
}
|
|
449
472
|
} catch (cockpitErr) {
|
|
450
473
|
console.log(` ${slate('(cockpit row unavailable — ' + cockpitErr.message + ' · PHEWSH_DEBUG=1 phewsh for details)')}`);
|
|
@@ -489,7 +512,8 @@ async function main() {
|
|
|
489
512
|
} catch { /* no settings = ambient off */ }
|
|
490
513
|
|
|
491
514
|
let pending = 0;
|
|
492
|
-
|
|
515
|
+
// Only substantive calls — never nag the user to "judge" a greeting.
|
|
516
|
+
try { pending = pendingDecisions({ project: projectName, substantive: true }).length; } catch { /* best-effort */ }
|
|
493
517
|
|
|
494
518
|
let installed = [];
|
|
495
519
|
try { installed = listHarnesses().filter(h => h.installed).map(h => h.id); } catch { /* best-effort */ }
|
|
@@ -644,7 +668,7 @@ async function main() {
|
|
|
644
668
|
});
|
|
645
669
|
lastTurnFailure = null;
|
|
646
670
|
awaitingOutcome = decisionId;
|
|
647
|
-
console.log(slate(` via ${HARNESSES[harnessId].label} ·
|
|
671
|
+
console.log(slate(` via ${HARNESSES[harnessId].label} · how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing`));
|
|
648
672
|
return true;
|
|
649
673
|
} catch (err) {
|
|
650
674
|
turnInFlight = false;
|
|
@@ -653,7 +677,7 @@ async function main() {
|
|
|
653
677
|
console.log(`\n ${slate('cancelled — esc')}`);
|
|
654
678
|
return true; // user's call, not a failure: no fallback offer
|
|
655
679
|
}
|
|
656
|
-
try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
|
|
680
|
+
try { labelOutcome(decisionId, 'failed', null, { auto: true }); } catch { /* keep going */ }
|
|
657
681
|
recordSessionEvent(harnessId, projectName, 'task_complete', {
|
|
658
682
|
taskId: decisionId, success: false, summary: input.slice(0, 140),
|
|
659
683
|
});
|
|
@@ -684,7 +708,7 @@ async function main() {
|
|
|
684
708
|
if (result.promptTokens) totalPromptTokens += result.promptTokens;
|
|
685
709
|
if (result.completionTokens) totalCompletionTokens += result.completionTokens;
|
|
686
710
|
if (result.promptTokens || result.completionTokens) {
|
|
687
|
-
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${modelName(currentModel)} ·
|
|
711
|
+
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${modelName(currentModel)} · how'd it go? 1-4 or keep typing`));
|
|
688
712
|
}
|
|
689
713
|
awaitingOutcome = decisionId;
|
|
690
714
|
trackSap({
|
|
@@ -705,7 +729,7 @@ async function main() {
|
|
|
705
729
|
console.log(`\n ${slate('cancelled — esc')}`);
|
|
706
730
|
return true; // user's call, not a failure: no fallback offer
|
|
707
731
|
}
|
|
708
|
-
try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
|
|
732
|
+
try { labelOutcome(decisionId, 'failed', null, { auto: true }); } catch { /* keep going */ }
|
|
709
733
|
messages.pop();
|
|
710
734
|
console.error(`\n ${ember('!')} ${sage('API route failed')}${slate(' — ' + err.message.split('\n')[0])}`);
|
|
711
735
|
return false;
|
|
@@ -812,10 +836,10 @@ async function main() {
|
|
|
812
836
|
// so you know it registered. Arguments stay plain. TTY-only, fail-soft.
|
|
813
837
|
const KNOWN_COMMANDS = new Set([
|
|
814
838
|
'quit', 'exit', 'q', 'help', 'h', 'init', 'intent', 'clarify', 'model',
|
|
815
|
-
'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'run',
|
|
839
|
+
'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'switch', 'run',
|
|
816
840
|
'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
|
|
817
841
|
'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
|
|
818
|
-
'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
842
|
+
'agents', 'context', 'truth', 'brief', 'wrap', 'reconcile', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
819
843
|
'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
|
|
820
844
|
]);
|
|
821
845
|
const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
|
|
@@ -990,19 +1014,23 @@ async function main() {
|
|
|
990
1014
|
return;
|
|
991
1015
|
}
|
|
992
1016
|
|
|
993
|
-
// /
|
|
994
|
-
if (
|
|
995
|
-
const
|
|
996
|
-
|
|
1017
|
+
// /reconcile confirmation: apply only the exact diff the user just saw.
|
|
1018
|
+
if (awaitingReconcile) {
|
|
1019
|
+
const proposal = awaitingReconcile;
|
|
1020
|
+
awaitingReconcile = null;
|
|
997
1021
|
if (/^(y|yes)$/i.test(input.trim())) {
|
|
998
|
-
const r =
|
|
1022
|
+
const r = applyReconciliation(proposal);
|
|
999
1023
|
if (r.written) {
|
|
1000
|
-
|
|
1024
|
+
const healed = selfheal.heal({ force: true });
|
|
1025
|
+
intentFiles = loadIntentContext();
|
|
1026
|
+
systemPrompt = buildSystemPrompt(intentFiles);
|
|
1027
|
+
console.log(` ${green('✓')} ${sage('Applied the approved diff to ' + r.target + '.')}`);
|
|
1028
|
+
console.log(` ${slate(healed.healed ? 'Generated Claude context refreshed from the approved intent change.' : 'Intent updated; generated context did not require a write.')}`);
|
|
1001
1029
|
} else {
|
|
1002
|
-
console.log(` ${ember('!')} ${sage('
|
|
1030
|
+
console.log(` ${ember('!')} ${sage('Reconciliation was not applied: ' + r.reason)}`);
|
|
1003
1031
|
}
|
|
1004
1032
|
} else {
|
|
1005
|
-
console.log(` ${slate('
|
|
1033
|
+
console.log(` ${slate('No authoritative files were changed.')}`);
|
|
1006
1034
|
}
|
|
1007
1035
|
console.log('');
|
|
1008
1036
|
rl.prompt();
|
|
@@ -1263,33 +1291,34 @@ async function main() {
|
|
|
1263
1291
|
return;
|
|
1264
1292
|
}
|
|
1265
1293
|
|
|
1266
|
-
// ── /wrap
|
|
1267
|
-
//
|
|
1268
|
-
// back into next.md, then re-sync CLAUDE.md — so the record reflects the
|
|
1269
|
-
// work, not last week. Deterministic from commit subjects; no LLM needed,
|
|
1270
|
-
// so it works even with nothing connected. This is the cure for the drift
|
|
1271
|
-
// that ate phewsh's own dogfood.
|
|
1294
|
+
// ── /wrap + /reconcile ─────────────────────────────
|
|
1295
|
+
// Wrap observes. Reconcile proposes. Only an explicit yes writes intent.
|
|
1272
1296
|
if (cmd === 'wrap') {
|
|
1273
|
-
const draft = selfheal.wrapDraft();
|
|
1274
1297
|
console.log('');
|
|
1275
|
-
|
|
1276
|
-
|
|
1298
|
+
const truth = await auditTruth();
|
|
1299
|
+
const observed = observeCurrent(truth);
|
|
1300
|
+
lastTransitionReport = observed;
|
|
1301
|
+
console.log(formatObservedReport(observed, { title: 'Wrap — observed current state' }));
|
|
1302
|
+
console.log('');
|
|
1303
|
+
rl.prompt();
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (cmd === 'reconcile') {
|
|
1308
|
+
console.log('');
|
|
1309
|
+
const report = lastTransitionReport || observeCurrent(await auditTruth());
|
|
1310
|
+
const proposal = reconciliationProposal(report);
|
|
1311
|
+
if (!proposal.available) {
|
|
1312
|
+
console.log(` ${ember('!')} ${sage('No reconciliation proposal: ' + proposal.reason)}`);
|
|
1277
1313
|
console.log('');
|
|
1278
1314
|
rl.prompt();
|
|
1279
1315
|
return;
|
|
1280
1316
|
}
|
|
1281
|
-
|
|
1282
|
-
console.log(
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (line.length > 70) line = line.slice(0, 69).trimEnd() + '…';
|
|
1287
|
-
console.log(` ${sage('+')} ${cream(line)}`);
|
|
1288
|
-
}
|
|
1289
|
-
if (n > 12) console.log(` ${slate('… and ' + (n - 12) + ' more')}`);
|
|
1290
|
-
ui.divider('line');
|
|
1291
|
-
console.log(` ${sage('Fold this into')} ${cream('.intent/next.md')} ${sage('and re-sync CLAUDE.md?')} ${slate('y/N')}`);
|
|
1292
|
-
awaitingWrap = { block: draft.block };
|
|
1317
|
+
console.log('Reconciliation proposal (no files changed):');
|
|
1318
|
+
console.log(proposal.diff);
|
|
1319
|
+
console.log('');
|
|
1320
|
+
console.log(` ${sage('Apply this exact diff?')} ${slate('y/N')}`);
|
|
1321
|
+
awaitingReconcile = proposal;
|
|
1293
1322
|
rl.prompt();
|
|
1294
1323
|
return;
|
|
1295
1324
|
}
|
|
@@ -1368,7 +1397,10 @@ async function main() {
|
|
|
1368
1397
|
console.log(` ${cream('author .intent/')}`);
|
|
1369
1398
|
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
1370
1399
|
console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
|
|
1371
|
-
console.log(` ${teal('/
|
|
1400
|
+
console.log(` ${teal('/truth')} ${sage('Read-only audit of versions, Git, intent, projections, and conflicts')}`);
|
|
1401
|
+
console.log(` ${teal('/brief')} ${sage('Generate the current provider-ready verified briefing')}`);
|
|
1402
|
+
console.log(` ${teal('/wrap')} ${sage('Observe changes, contradictions, unknowns, and reconciliation needs')}`);
|
|
1403
|
+
console.log(` ${teal('/reconcile')} ${sage('Propose an exact intent diff; writes only after approval')}`);
|
|
1372
1404
|
console.log(` ${teal('/clarify')} ${sage('Turn ideas into .intent/ artifacts')}`);
|
|
1373
1405
|
console.log(` ${teal('/gate')} ${sage('Set constraints (budget, time, skill)')}`);
|
|
1374
1406
|
console.log(` ${teal('/context')} ${sage('Show loaded .intent/ files')}`);
|
|
@@ -1391,10 +1423,11 @@ async function main() {
|
|
|
1391
1423
|
console.log(` ${teal('/harnesses')} ${sage('Agent CLIs detected on this machine')}`);
|
|
1392
1424
|
console.log(` ${teal('/provider')} ${sage('Current route + what\'s available')}`);
|
|
1393
1425
|
console.log(` ${teal('/fallback')} ${sage('What happens at a usage wall: ask or auto-switch')}`);
|
|
1394
|
-
console.log(` ${teal('/outcomes')} ${sage('
|
|
1426
|
+
console.log(` ${teal('/outcomes')} ${sage('What worked — tell phewsh, and it learns which tool to trust')}`);
|
|
1395
1427
|
console.log('');
|
|
1396
1428
|
console.log(` ${cream('session')}`);
|
|
1397
|
-
console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('
|
|
1429
|
+
console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('Preflight, brief, native handoff, automatic postflight')}`);
|
|
1430
|
+
console.log(` ${teal('/switch')} ${slate('<harness>')} ${sage('Launch another native tool with a freshly verified briefing')}`);
|
|
1398
1431
|
console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
|
|
1399
1432
|
console.log(` ${teal('esc')} ${sage('Cancel a running turn · clear the input line')}`);
|
|
1400
1433
|
console.log(` ${teal('/clear')} ${sage('Clear conversation')}`);
|
|
@@ -1450,6 +1483,7 @@ async function main() {
|
|
|
1450
1483
|
ui.divider('line');
|
|
1451
1484
|
intentFiles.forEach(f => console.log(` ${teal('●')} ${cream(f.file)} ${slate('(' + f.content.length + ' chars)')}`));
|
|
1452
1485
|
ui.divider('line');
|
|
1486
|
+
console.log(formatSourceContract({ compact: true }).split('\n').map(line => ` ${line}`).join('\n'));
|
|
1453
1487
|
} else {
|
|
1454
1488
|
console.log(`\n ${sage('No .intent/ context found in')} ${slate(process.cwd())}`);
|
|
1455
1489
|
console.log(` ${sage('Run')} ${cream('/init')} ${sage('to create one')}`);
|
|
@@ -1459,6 +1493,23 @@ async function main() {
|
|
|
1459
1493
|
return;
|
|
1460
1494
|
}
|
|
1461
1495
|
|
|
1496
|
+
if (cmd === 'truth') {
|
|
1497
|
+
console.log('');
|
|
1498
|
+
console.log(formatTruth(await auditTruth()));
|
|
1499
|
+
console.log('');
|
|
1500
|
+
rl.prompt();
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (cmd === 'brief') {
|
|
1505
|
+
console.log('');
|
|
1506
|
+
const { content } = await generateBrief();
|
|
1507
|
+
console.log(content);
|
|
1508
|
+
console.log('');
|
|
1509
|
+
rl.prompt();
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1462
1513
|
if (cmd === 'status') {
|
|
1463
1514
|
const turns = messages.length / 2;
|
|
1464
1515
|
config = loadConfig();
|
|
@@ -2020,9 +2071,9 @@ async function main() {
|
|
|
2020
2071
|
messages.push({ role: 'user', content: cmdArg });
|
|
2021
2072
|
messages.push({ role: 'assistant', content: `[council of ${answers.length}]\n\n${answers.join('\n\n')}` });
|
|
2022
2073
|
awaitingOutcome = decisionId;
|
|
2023
|
-
console.log(slate(` council of ${answers.length} ·
|
|
2074
|
+
console.log(slate(` council of ${answers.length} · how'd it go? 1 kept · 2 undid · 3 redid · 4 flopped · or keep typing`));
|
|
2024
2075
|
} else {
|
|
2025
|
-
try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
|
|
2076
|
+
try { labelOutcome(decisionId, 'failed', null, { auto: true }); } catch { /* keep going */ }
|
|
2026
2077
|
console.log(` ${sage('Every council member failed — check')} ${cream('/provider')}`);
|
|
2027
2078
|
}
|
|
2028
2079
|
console.log('');
|
|
@@ -2227,10 +2278,9 @@ async function main() {
|
|
|
2227
2278
|
return;
|
|
2228
2279
|
}
|
|
2229
2280
|
|
|
2230
|
-
if (cmd === 'work') {
|
|
2231
|
-
//
|
|
2232
|
-
//
|
|
2233
|
-
// phewsh stays the front door AND the return point.
|
|
2281
|
+
if (cmd === 'work' || cmd === 'switch') {
|
|
2282
|
+
// Native handoff lifecycle: verify -> brief -> release the terminal ->
|
|
2283
|
+
// observe -> reconcile. The harness owns its UI while it runs.
|
|
2234
2284
|
const target = cmdArg?.trim().toLowerCase() || (route?.type === 'harness' ? route.id : 'claude-code');
|
|
2235
2285
|
const h = HARNESSES[target];
|
|
2236
2286
|
if (!h) {
|
|
@@ -2243,6 +2293,13 @@ async function main() {
|
|
|
2243
2293
|
rl.prompt();
|
|
2244
2294
|
return;
|
|
2245
2295
|
}
|
|
2296
|
+
|
|
2297
|
+
const preflightTruth = await auditTruth();
|
|
2298
|
+
const before = captureSnapshot(preflightTruth);
|
|
2299
|
+
const generatedBrief = await generateBrief({ report: preflightTruth });
|
|
2300
|
+
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.`;
|
|
2301
|
+
const savedBrief = persistBrief(generatedBrief.content, { project: projectName, route: target });
|
|
2302
|
+
const launch = interactiveLaunchArgs(target, launchBrief, { model: harnessModel });
|
|
2246
2303
|
const decisionId = recordDecision({
|
|
2247
2304
|
project: projectName,
|
|
2248
2305
|
route: target,
|
|
@@ -2251,23 +2308,49 @@ async function main() {
|
|
|
2251
2308
|
});
|
|
2252
2309
|
decisionsThisSession++;
|
|
2253
2310
|
console.log('');
|
|
2254
|
-
console.log(` ${
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
}
|
|
2311
|
+
console.log(` ${b(cream('Work preflight'))} ${slate('— verified before native handoff')}`);
|
|
2312
|
+
ui.divider('line');
|
|
2313
|
+
console.log(` ${sage('Route:')} ${cream(h.label)}${harnessModel ? slate(' · model ' + harnessModel) : slate(' · native default model')}`);
|
|
2314
|
+
console.log(` ${sage('Git:')} ${cream(preflightTruth.git.shortHead || 'unknown')} ${slate('· ' + before.dirty.length + ' uncommitted path(s) before work')}`);
|
|
2315
|
+
console.log(` ${sage('Truth:')} ${preflightTruth.conflicts.length ? peach(preflightTruth.conflicts.length + ' conflict(s) carried explicitly') : green('no explicit conflicts')}`);
|
|
2316
|
+
console.log(` ${sage('Brief:')} ${launch.briefingPassed ? green('passed directly to the native harness') : peach('not injectable for this harness; available via /brief')}`);
|
|
2317
|
+
console.log(` ${sage('Record:')} ${savedBrief.written ? slate('exact briefing saved locally for this transition') : peach('briefing persistence unavailable; launch continues')}`);
|
|
2318
|
+
console.log(` ${slate('After exit PHEWSH will compare Git, files, intent claims, generated drift, and contradictions.')}`);
|
|
2319
|
+
ui.divider('line');
|
|
2320
|
+
console.log(` ${teal('●')} ${sage('Handing the terminal to')} ${cream(h.label)} ${slate('— exit to return for postflight')}`);
|
|
2258
2321
|
console.log('');
|
|
2322
|
+
recordSessionEvent(target, projectName, 'work_started', {
|
|
2323
|
+
taskId: decisionId,
|
|
2324
|
+
summary: `interactive ${h.label} session`,
|
|
2325
|
+
gitHead: before.head,
|
|
2326
|
+
dirtyPaths: before.dirty,
|
|
2327
|
+
briefingHash: savedBrief.hash,
|
|
2328
|
+
briefingFile: savedBrief.file,
|
|
2329
|
+
briefingPassed: launch.briefingPassed,
|
|
2330
|
+
});
|
|
2259
2331
|
pasteMode(false);
|
|
2260
2332
|
rl.pause();
|
|
2261
2333
|
const { spawnSync } = require('child_process');
|
|
2262
|
-
const res = spawnSync(h.bin,
|
|
2334
|
+
const res = spawnSync(h.bin, launch.args, { stdio: 'inherit' });
|
|
2263
2335
|
rl.resume();
|
|
2264
2336
|
pasteMode(true);
|
|
2337
|
+
const postflight = await createPostflight(before);
|
|
2338
|
+
lastTransitionReport = postflight;
|
|
2265
2339
|
recordSessionEvent(target, projectName, 'task_complete', {
|
|
2266
|
-
taskId: decisionId,
|
|
2340
|
+
taskId: decisionId,
|
|
2341
|
+
success: res.status === 0,
|
|
2342
|
+
summary: `interactive ${h.label} session`,
|
|
2343
|
+
gitHeadBefore: before.head,
|
|
2344
|
+
gitHeadAfter: postflight.afterHead,
|
|
2345
|
+
changedFiles: postflight.files,
|
|
2346
|
+
conflicts: postflight.conflicts,
|
|
2347
|
+
briefingHash: savedBrief.hash,
|
|
2267
2348
|
});
|
|
2268
2349
|
awaitingOutcome = decisionId;
|
|
2269
2350
|
console.log('');
|
|
2270
|
-
console.log(
|
|
2351
|
+
console.log(formatObservedReport(postflight, { title: `${h.label} session ended — postflight` }));
|
|
2352
|
+
console.log('');
|
|
2353
|
+
console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate("1 kept · 2 undid · 3 redid · 4 flopped · /reconcile · /switch <tool>")}`);
|
|
2271
2354
|
console.log('');
|
|
2272
2355
|
rl.prompt();
|
|
2273
2356
|
return;
|