phewsh 0.15.29 → 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 +15 -2
- package/commands/session.js +139 -60
- 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 +31 -8
- 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/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
|
@@ -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,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,6 +443,20 @@ 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
461
|
if (oStats.total > 0) {
|
|
442
462
|
// Plain language: what phewsh has logged vs what you've taught it. The
|
|
@@ -816,10 +836,10 @@ async function main() {
|
|
|
816
836
|
// so you know it registered. Arguments stay plain. TTY-only, fail-soft.
|
|
817
837
|
const KNOWN_COMMANDS = new Set([
|
|
818
838
|
'quit', 'exit', 'q', 'help', 'h', 'init', 'intent', 'clarify', 'model',
|
|
819
|
-
'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'run',
|
|
839
|
+
'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'switch', 'run',
|
|
820
840
|
'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
|
|
821
841
|
'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
|
|
822
|
-
'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
842
|
+
'agents', 'context', 'truth', 'brief', 'wrap', 'reconcile', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
823
843
|
'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
|
|
824
844
|
]);
|
|
825
845
|
const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
|
|
@@ -994,19 +1014,23 @@ async function main() {
|
|
|
994
1014
|
return;
|
|
995
1015
|
}
|
|
996
1016
|
|
|
997
|
-
// /
|
|
998
|
-
if (
|
|
999
|
-
const
|
|
1000
|
-
|
|
1017
|
+
// /reconcile confirmation: apply only the exact diff the user just saw.
|
|
1018
|
+
if (awaitingReconcile) {
|
|
1019
|
+
const proposal = awaitingReconcile;
|
|
1020
|
+
awaitingReconcile = null;
|
|
1001
1021
|
if (/^(y|yes)$/i.test(input.trim())) {
|
|
1002
|
-
const r =
|
|
1022
|
+
const r = applyReconciliation(proposal);
|
|
1003
1023
|
if (r.written) {
|
|
1004
|
-
|
|
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.')}`);
|
|
1005
1029
|
} else {
|
|
1006
|
-
console.log(` ${ember('!')} ${sage('
|
|
1030
|
+
console.log(` ${ember('!')} ${sage('Reconciliation was not applied: ' + r.reason)}`);
|
|
1007
1031
|
}
|
|
1008
1032
|
} else {
|
|
1009
|
-
console.log(` ${slate('
|
|
1033
|
+
console.log(` ${slate('No authoritative files were changed.')}`);
|
|
1010
1034
|
}
|
|
1011
1035
|
console.log('');
|
|
1012
1036
|
rl.prompt();
|
|
@@ -1267,33 +1291,34 @@ async function main() {
|
|
|
1267
1291
|
return;
|
|
1268
1292
|
}
|
|
1269
1293
|
|
|
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.
|
|
1294
|
+
// ── /wrap + /reconcile ─────────────────────────────
|
|
1295
|
+
// Wrap observes. Reconcile proposes. Only an explicit yes writes intent.
|
|
1276
1296
|
if (cmd === 'wrap') {
|
|
1277
|
-
const draft = selfheal.wrapDraft();
|
|
1278
1297
|
console.log('');
|
|
1279
|
-
|
|
1280
|
-
|
|
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)}`);
|
|
1281
1313
|
console.log('');
|
|
1282
1314
|
rl.prompt();
|
|
1283
1315
|
return;
|
|
1284
1316
|
}
|
|
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 };
|
|
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;
|
|
1297
1322
|
rl.prompt();
|
|
1298
1323
|
return;
|
|
1299
1324
|
}
|
|
@@ -1372,7 +1397,10 @@ async function main() {
|
|
|
1372
1397
|
console.log(` ${cream('author .intent/')}`);
|
|
1373
1398
|
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
1374
1399
|
console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
|
|
1375
|
-
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')}`);
|
|
1376
1404
|
console.log(` ${teal('/clarify')} ${sage('Turn ideas into .intent/ artifacts')}`);
|
|
1377
1405
|
console.log(` ${teal('/gate')} ${sage('Set constraints (budget, time, skill)')}`);
|
|
1378
1406
|
console.log(` ${teal('/context')} ${sage('Show loaded .intent/ files')}`);
|
|
@@ -1398,7 +1426,8 @@ async function main() {
|
|
|
1398
1426
|
console.log(` ${teal('/outcomes')} ${sage('What worked — tell phewsh, and it learns which tool to trust')}`);
|
|
1399
1427
|
console.log('');
|
|
1400
1428
|
console.log(` ${cream('session')}`);
|
|
1401
|
-
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')}`);
|
|
1402
1431
|
console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
|
|
1403
1432
|
console.log(` ${teal('esc')} ${sage('Cancel a running turn · clear the input line')}`);
|
|
1404
1433
|
console.log(` ${teal('/clear')} ${sage('Clear conversation')}`);
|
|
@@ -1454,6 +1483,7 @@ async function main() {
|
|
|
1454
1483
|
ui.divider('line');
|
|
1455
1484
|
intentFiles.forEach(f => console.log(` ${teal('●')} ${cream(f.file)} ${slate('(' + f.content.length + ' chars)')}`));
|
|
1456
1485
|
ui.divider('line');
|
|
1486
|
+
console.log(formatSourceContract({ compact: true }).split('\n').map(line => ` ${line}`).join('\n'));
|
|
1457
1487
|
} else {
|
|
1458
1488
|
console.log(`\n ${sage('No .intent/ context found in')} ${slate(process.cwd())}`);
|
|
1459
1489
|
console.log(` ${sage('Run')} ${cream('/init')} ${sage('to create one')}`);
|
|
@@ -1463,6 +1493,23 @@ async function main() {
|
|
|
1463
1493
|
return;
|
|
1464
1494
|
}
|
|
1465
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
|
+
|
|
1466
1513
|
if (cmd === 'status') {
|
|
1467
1514
|
const turns = messages.length / 2;
|
|
1468
1515
|
config = loadConfig();
|
|
@@ -2231,10 +2278,9 @@ async function main() {
|
|
|
2231
2278
|
return;
|
|
2232
2279
|
}
|
|
2233
2280
|
|
|
2234
|
-
if (cmd === 'work') {
|
|
2235
|
-
//
|
|
2236
|
-
//
|
|
2237
|
-
// 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.
|
|
2238
2284
|
const target = cmdArg?.trim().toLowerCase() || (route?.type === 'harness' ? route.id : 'claude-code');
|
|
2239
2285
|
const h = HARNESSES[target];
|
|
2240
2286
|
if (!h) {
|
|
@@ -2247,6 +2293,13 @@ async function main() {
|
|
|
2247
2293
|
rl.prompt();
|
|
2248
2294
|
return;
|
|
2249
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 });
|
|
2250
2303
|
const decisionId = recordDecision({
|
|
2251
2304
|
project: projectName,
|
|
2252
2305
|
route: target,
|
|
@@ -2255,23 +2308,49 @@ async function main() {
|
|
|
2255
2308
|
});
|
|
2256
2309
|
decisionsThisSession++;
|
|
2257
2310
|
console.log('');
|
|
2258
|
-
console.log(` ${
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
}
|
|
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')}`);
|
|
2262
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
|
+
});
|
|
2263
2331
|
pasteMode(false);
|
|
2264
2332
|
rl.pause();
|
|
2265
2333
|
const { spawnSync } = require('child_process');
|
|
2266
|
-
const res = spawnSync(h.bin,
|
|
2334
|
+
const res = spawnSync(h.bin, launch.args, { stdio: 'inherit' });
|
|
2267
2335
|
rl.resume();
|
|
2268
2336
|
pasteMode(true);
|
|
2337
|
+
const postflight = await createPostflight(before);
|
|
2338
|
+
lastTransitionReport = postflight;
|
|
2269
2339
|
recordSessionEvent(target, projectName, 'task_complete', {
|
|
2270
|
-
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,
|
|
2271
2348
|
});
|
|
2272
2349
|
awaitingOutcome = decisionId;
|
|
2273
2350
|
console.log('');
|
|
2274
|
-
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>")}`);
|
|
2275
2354
|
console.log('');
|
|
2276
2355
|
rl.prompt();
|
|
2277
2356
|
return;
|
|
@@ -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
|
+
});
|
package/lib/brief.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { auditTruth } = require('./truth');
|
|
6
|
+
|
|
7
|
+
function concise(value, max = 280) {
|
|
8
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
9
|
+
return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatBrief(report, { cwd = process.cwd(), maxClaims = 5 } = {}) {
|
|
13
|
+
const lines = [];
|
|
14
|
+
const npm = report.package.npmLatest.status === 'known'
|
|
15
|
+
? report.package.npmLatest.version
|
|
16
|
+
: `unknown (${report.package.npmLatest.reason})`;
|
|
17
|
+
const dirtyCount = report.git.tracked.length + report.git.untracked.length;
|
|
18
|
+
|
|
19
|
+
lines.push('# PHEWSH Verified Project Brief');
|
|
20
|
+
lines.push(`Project: ${path.basename(cwd)}`);
|
|
21
|
+
lines.push(`Generated: ${report.auditedAt}`);
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push('## Verified State');
|
|
24
|
+
lines.push(`- Git HEAD: ${report.git.available ? report.git.shortHead : 'unknown'}`);
|
|
25
|
+
if (report.git.committedPackageVersion) lines.push(`- Package at Git HEAD: ${report.git.committedPackageVersion}`);
|
|
26
|
+
lines.push(`- Working package: ${report.package.version}`);
|
|
27
|
+
lines.push(`- npm latest: ${npm}`);
|
|
28
|
+
lines.push(`- Working tree: ${dirtyCount ? `${dirtyCount} changed path(s), uncommitted` : 'clean'}`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
lines.push('## Intent Claims');
|
|
31
|
+
lines.push('These sources are authoritative for purpose and priorities, but code/release claims below remain claims until verified.');
|
|
32
|
+
report.intent.authoritative.slice(0, maxClaims).forEach(claim => {
|
|
33
|
+
lines.push(`- ${claim.file} [declared ${claim.declaredUpdated || 'unknown'}]: ${concise(claim.summary)}`);
|
|
34
|
+
});
|
|
35
|
+
lines.push('');
|
|
36
|
+
lines.push('## Conflicts And Unknowns');
|
|
37
|
+
if (!report.conflicts.length) lines.push('- No explicit conflicts detected.');
|
|
38
|
+
report.conflicts.forEach(item => lines.push(`- Conflict: ${concise(item, 360)}`));
|
|
39
|
+
if (!report.unknowns.length) lines.push('- No additional unknowns recorded.');
|
|
40
|
+
report.unknowns.slice(0, 6).forEach(item => lines.push(`- Unknown: ${concise(item, 360)}`));
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push('## Continuity Evidence');
|
|
43
|
+
lines.push(`- Outcomes: ${report.outcomes.total} routed; ${report.outcomes.judged} human-judged; ${report.outcomes.pending} pending.`);
|
|
44
|
+
lines.push(`- Receipts: ${report.receipts.totalEvents} local event(s); ${report.receipts.completed} completed; ${report.receipts.failed} failed.`);
|
|
45
|
+
for (const item of report.outcomes.recent.slice(0, 3)) {
|
|
46
|
+
lines.push(`- Recent ${item.route || 'unknown'}: ${concise(item.summary, 180)}${item.outcome ? ` [${item.outcome}]` : ' [pending]'}`);
|
|
47
|
+
}
|
|
48
|
+
lines.push('');
|
|
49
|
+
lines.push('## Operating Contract');
|
|
50
|
+
lines.push('- Prefer verified runtime and repository evidence over generated summaries or model memory.');
|
|
51
|
+
lines.push('- Treat .intent/ as authoritative for goals and declared decisions, not as proof that implementation shipped.');
|
|
52
|
+
lines.push('- Do not describe working-tree changes as committed, published, or deployed.');
|
|
53
|
+
lines.push('- Surface disagreement explicitly. Do not silently merge conflicting sources.');
|
|
54
|
+
lines.push('- Before finishing, report what changed, what remains unknown, and what should be reconciled.');
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function generateBrief(options = {}) {
|
|
59
|
+
const report = options.report || await auditTruth(options);
|
|
60
|
+
return { report, content: formatBrief(report, options) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function persistBrief(content, {
|
|
64
|
+
project = path.basename(process.cwd()),
|
|
65
|
+
route = 'unknown',
|
|
66
|
+
root = path.join(os.homedir(), '.phewsh', 'briefs'),
|
|
67
|
+
} = {}) {
|
|
68
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
69
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
70
|
+
const dir = path.join(root, project);
|
|
71
|
+
const file = path.join(dir, `${stamp}-${route}.md`);
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
fs.writeFileSync(file, content);
|
|
75
|
+
return { written: true, file, hash };
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return { written: false, file: null, hash, reason: err.message };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { concise, formatBrief, generateBrief, persistBrief };
|
package/lib/harnesses.js
CHANGED
|
@@ -20,11 +20,11 @@ const { execSync, spawn } = require('child_process');
|
|
|
20
20
|
// list of its own, so it can never go stale. Harnesses without a known
|
|
21
21
|
// model flag ignore the preference and use their own config.
|
|
22
22
|
const HARNESSES = {
|
|
23
|
-
'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, modelHints: ['sonnet', 'opus', 'haiku'], streamFormat: 'claude-json', args: (p, m) => ['-p', p, '--output-format', 'stream-json', '--include-partial-messages', '--verbose', ...(m ? ['--model', m] : [])] },
|
|
24
|
-
'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', '--skip-git-repo-check', ...(m ? ['-m', m] : []), p] },
|
|
25
|
-
'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', models: true, args: (p, m) => ['-p', p, ...(m ? ['-m', m] : [])] },
|
|
23
|
+
'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, modelHints: ['sonnet', 'opus', 'haiku'], streamFormat: 'claude-json', args: (p, m) => ['-p', p, '--output-format', 'stream-json', '--include-partial-messages', '--verbose', ...(m ? ['--model', m] : [])], interactiveArgs: (brief, m) => ['--append-system-prompt', brief, ...(m ? ['--model', m] : [])] },
|
|
24
|
+
'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', '--skip-git-repo-check', ...(m ? ['-m', m] : []), p], interactiveArgs: (brief, m) => [...(m ? ['-m', m] : []), brief] },
|
|
25
|
+
'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', models: true, args: (p, m) => ['-p', p, ...(m ? ['-m', m] : [])], interactiveArgs: (brief, m) => ['--prompt-interactive', brief, ...(m ? ['--model', m] : [])] },
|
|
26
26
|
'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', auth: 'Cursor account', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
|
|
27
|
-
'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', auth: 'OpenCode Zen / configured', args: (p) => ['run', p] },
|
|
27
|
+
'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', auth: 'OpenCode Zen / configured', args: (p) => ['run', p], interactiveArgs: (brief) => ['--prompt', brief] },
|
|
28
28
|
'grok': { bin: 'grok', label: 'Grok Build', role: "xAI's take", auth: 'SuperGrok / X Premium+', args: (p) => ['-p', p] },
|
|
29
29
|
'kiro': { bin: 'kiro-cli', label: 'Kiro CLI', role: 'spec-driven dev', auth: 'Kiro / AWS account', args: (p) => ['chat', '--no-interactive', p] },
|
|
30
30
|
'copilot': { bin: 'copilot', label: 'Copilot CLI', role: 'github-native', auth: 'GitHub Copilot plan', args: (p) => ['-p', p] },
|
|
@@ -75,6 +75,16 @@ function listHarnesses() {
|
|
|
75
75
|
return Object.entries(HARNESSES).map(([id, h]) => ({ id, ...h, headless: !!h.args, installed: isInstalled(id) }));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function interactiveLaunchArgs(id, brief, { model } = {}) {
|
|
79
|
+
const h = HARNESSES[id];
|
|
80
|
+
if (!h) throw new Error(`Unknown harness: ${id}`);
|
|
81
|
+
if (!h.interactiveArgs) return { args: [], briefingPassed: false };
|
|
82
|
+
return {
|
|
83
|
+
args: h.interactiveArgs(brief, h.models ? model : undefined),
|
|
84
|
+
briefingPassed: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
/**
|
|
79
89
|
* Run a prompt through a harness, streaming stdout to the terminal.
|
|
80
90
|
* stderr is buffered and only surfaced on failure (codex/gemini chat on it).
|
|
@@ -187,4 +197,12 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
|
187
197
|
});
|
|
188
198
|
}
|
|
189
199
|
|
|
190
|
-
module.exports = {
|
|
200
|
+
module.exports = {
|
|
201
|
+
HARNESSES,
|
|
202
|
+
isInstalled,
|
|
203
|
+
detectInstalled,
|
|
204
|
+
interactiveLaunchArgs,
|
|
205
|
+
listHarnesses,
|
|
206
|
+
runViaHarness,
|
|
207
|
+
cancelActive,
|
|
208
|
+
};
|