phewsh 0.15.24 → 0.15.26
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/commands/session.js +45 -1
- package/lib/outcomes.js +4 -1
- package/lib/recall.js +59 -0
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -22,6 +22,7 @@ const { recordDecision, labelOutcome, pendingDecisions, recentDecisions, outcome
|
|
|
22
22
|
const { suggest, suggestAll } = require('../lib/suggest');
|
|
23
23
|
const continuity = require('../lib/continuity');
|
|
24
24
|
const learning = require('../lib/learning');
|
|
25
|
+
const recall = require('../lib/recall');
|
|
25
26
|
const { closest } = require('../lib/closest');
|
|
26
27
|
const cmdHistory = require('../lib/history');
|
|
27
28
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
@@ -333,6 +334,7 @@ async function main() {
|
|
|
333
334
|
let route = resolveRoute(config, harnesses);
|
|
334
335
|
let sessionMode = null; // INTENT_MODES id once picked
|
|
335
336
|
let awaitingOutcome = null; // decision id eligible for 1-4 labeling
|
|
337
|
+
let awaitingWhy = null; // { id, outcome } — next line is the reason
|
|
336
338
|
let awaitingFallback = null; // { input, fullSystem, options } after a route failure
|
|
337
339
|
let bootstrapChoices = null; // root-bootstrap menu entries when no project here
|
|
338
340
|
let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
|
|
@@ -583,7 +585,26 @@ async function main() {
|
|
|
583
585
|
const failureTracker = createFailureTracker();
|
|
584
586
|
let lastTurnFailure = null;
|
|
585
587
|
|
|
588
|
+
// The gate looking backward: if this is close to something you already
|
|
589
|
+
// reverted or failed, say so once — quietly, before the turn runs.
|
|
590
|
+
let lastRecallId = null;
|
|
591
|
+
function recallHeadsUp(input) {
|
|
592
|
+
try {
|
|
593
|
+
const past = recentDecisions(300, { project: projectName });
|
|
594
|
+
const hit = recall.closestRegret(past, input, { project: projectName, minSimilarity: 0.5 });
|
|
595
|
+
if (!hit || hit.id === lastRecallId) return;
|
|
596
|
+
lastRecallId = hit.id;
|
|
597
|
+
let s = (hit.summary || '').replace(/\s+/g, ' ');
|
|
598
|
+
if (s.length > 50) s = s.slice(0, 49).trimEnd() + '…';
|
|
599
|
+
const verb = hit.outcome === 'failed' ? 'failed' : 'reverted';
|
|
600
|
+
console.log(` ${peach('↩')} ${sage(`You ${verb} something close before:`)} ${slate('“' + s + '” · via ' + continuity.labelFor(hit.route) + ' · ' + continuity.agoText(hit.ts))}`);
|
|
601
|
+
if (hit.why) console.log(` ${slate(' why it didn\'t hold: ')}${sage(hit.why)}`);
|
|
602
|
+
console.log(` ${slate(' not a block — just so the record doesn\'t let you repeat it blind.')}`);
|
|
603
|
+
} catch { /* recall is advisory, never blocks a turn */ }
|
|
604
|
+
}
|
|
605
|
+
|
|
586
606
|
async function runHarnessTurn(input, harnessId, fullSystem) {
|
|
607
|
+
recallHeadsUp(input);
|
|
587
608
|
const decisionId = recordDecision({
|
|
588
609
|
project: projectName, route: harnessId, mode: sessionMode, summary: input,
|
|
589
610
|
});
|
|
@@ -623,6 +644,7 @@ async function main() {
|
|
|
623
644
|
}
|
|
624
645
|
|
|
625
646
|
async function runApiTurn(input, fullSystem) {
|
|
647
|
+
recallHeadsUp(input);
|
|
626
648
|
const decisionId = recordDecision({
|
|
627
649
|
project: projectName, route: 'api', mode: sessionMode, summary: input,
|
|
628
650
|
});
|
|
@@ -935,6 +957,16 @@ async function main() {
|
|
|
935
957
|
async function handleInput(input) {
|
|
936
958
|
input = expandPastes(input);
|
|
937
959
|
|
|
960
|
+
// Answering the "why?" prompt — the whole line is the reason, not a command.
|
|
961
|
+
if (awaitingWhy) {
|
|
962
|
+
const { id, outcome } = awaitingWhy;
|
|
963
|
+
awaitingWhy = null;
|
|
964
|
+
try { labelOutcome(id, outcome, input); console.log(` ${slate('noted — the record will remember why.')}`); } catch { /* gone */ }
|
|
965
|
+
console.log('');
|
|
966
|
+
rl.prompt();
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
938
970
|
// Any typed input supersedes a pending "did you mean" offer.
|
|
939
971
|
if (pendingDidYouMean) pendingDidYouMean = null;
|
|
940
972
|
|
|
@@ -959,12 +991,22 @@ async function main() {
|
|
|
959
991
|
// A bare 1-4 right after a routed action labels its outcome
|
|
960
992
|
if (awaitingOutcome && /^[1-4]$/.test(input)) {
|
|
961
993
|
const outcome = OUTCOMES[parseInt(input, 10) - 1];
|
|
994
|
+
let labeled = null;
|
|
962
995
|
try {
|
|
963
|
-
labelOutcome(awaitingOutcome, outcome);
|
|
996
|
+
labeled = labelOutcome(awaitingOutcome, outcome);
|
|
964
997
|
const color = outcome === 'kept' ? green : outcome === 'superseded' ? peach : ember;
|
|
965
998
|
console.log(` ${teal('●')} ${sage('outcome:')} ${color(outcome)}`);
|
|
966
999
|
} catch { /* decision vanished — nothing to do */ }
|
|
1000
|
+
const id = awaitingOutcome;
|
|
967
1001
|
awaitingOutcome = null;
|
|
1002
|
+
// For a regret, the reason is the gold — capture one line so /recall can
|
|
1003
|
+
// tell you *why* next time. Kept/superseded stay frictionless.
|
|
1004
|
+
if (labeled && (outcome === 'reverted' || outcome === 'failed')) {
|
|
1005
|
+
awaitingWhy = { id, outcome };
|
|
1006
|
+
console.log(` ${slate('why? one line — feeds /recall next time · enter to skip')}`);
|
|
1007
|
+
rl.prompt();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
968
1010
|
console.log('');
|
|
969
1011
|
rl.prompt();
|
|
970
1012
|
return;
|
|
@@ -2248,6 +2290,8 @@ async function main() {
|
|
|
2248
2290
|
const lineDispatcher = createLineDispatcher(handleInput, {
|
|
2249
2291
|
onBatch: ({ input, lines }) => { collapsePastedEcho(lines, input); cmdHistory.append(input); },
|
|
2250
2292
|
onNoop: () => {
|
|
2293
|
+
// Bare Enter skips the "why?" prompt — the label still stands.
|
|
2294
|
+
if (awaitingWhy) { awaitingWhy = null; console.log(''); rl.prompt(); return; }
|
|
2251
2295
|
// Bare Enter accepts a pending "did you mean" suggestion.
|
|
2252
2296
|
if (pendingDidYouMean) {
|
|
2253
2297
|
const cmd = pendingDidYouMean;
|
package/lib/outcomes.js
CHANGED
|
@@ -51,7 +51,7 @@ function recordDecision({ project, route, mode, summary }) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Label a decision by id (or unambiguous id prefix). Returns the decision or null. */
|
|
54
|
-
function labelOutcome(idOrPrefix, outcome) {
|
|
54
|
+
function labelOutcome(idOrPrefix, outcome, why = null) {
|
|
55
55
|
if (!OUTCOMES.includes(outcome)) {
|
|
56
56
|
throw new Error(`Outcome must be one of: ${OUTCOMES.join(', ')}`);
|
|
57
57
|
}
|
|
@@ -60,6 +60,9 @@ function labelOutcome(idOrPrefix, outcome) {
|
|
|
60
60
|
if (matches.length !== 1) return null;
|
|
61
61
|
matches[0].outcome = outcome;
|
|
62
62
|
matches[0].labeledAt = new Date().toISOString();
|
|
63
|
+
// One-line reason — the most valuable thing about a reverted/failed call.
|
|
64
|
+
// Feeds /recall ("you reverted this before — because …") and /learn.
|
|
65
|
+
if (why && String(why).trim()) matches[0].why = String(why).trim().slice(0, 200);
|
|
63
66
|
save(decisions);
|
|
64
67
|
return matches[0];
|
|
65
68
|
}
|
package/lib/recall.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Recall — the record warning you before you repeat a mistake.
|
|
2
|
+
//
|
|
3
|
+
// Every decision is labeled. When you're about to do something close to what
|
|
4
|
+
// you already tried and *reverted* or *failed*, phewsh should say so — once,
|
|
5
|
+
// quietly, before you spend the turn. This is the decision gate looking
|
|
6
|
+
// backward: "you've been here; it didn't hold."
|
|
7
|
+
//
|
|
8
|
+
// Pure: feed it past decisions + the new text, get back the closest prior
|
|
9
|
+
// regret, or null. Similarity is token-overlap (Jaccard) so it matches intent
|
|
10
|
+
// ("add dark mode toggle" ≈ "build the dark-mode switch"), not exact strings.
|
|
11
|
+
|
|
12
|
+
const STOP = new Set([
|
|
13
|
+
'the', 'a', 'an', 'to', 'of', 'and', 'or', 'for', 'in', 'on', 'at', 'with',
|
|
14
|
+
'is', 'it', 'this', 'that', 'i', 'we', 'my', 'our', 'me', 'be', 'do', 'can',
|
|
15
|
+
'will', 'would', 'should', 'let', 'lets', 'please', 'just', 'make', 'add',
|
|
16
|
+
'use', 'using', 'get', 'set', 'so', 'as', 'by', 'from', 'into', 'up',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const REGRET = new Set(['reverted', 'failed']);
|
|
20
|
+
|
|
21
|
+
function tokens(s) {
|
|
22
|
+
return new Set(
|
|
23
|
+
String(s || '')
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.split(/[^a-z0-9]+/)
|
|
26
|
+
.filter((w) => w.length > 2 && !STOP.has(w))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Jaccard overlap of two strings' meaningful tokens, 0..1. */
|
|
31
|
+
function similarity(a, b) {
|
|
32
|
+
const A = tokens(a), B = tokens(b);
|
|
33
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
34
|
+
let inter = 0;
|
|
35
|
+
for (const t of A) if (B.has(t)) inter++;
|
|
36
|
+
return inter / (A.size + B.size - inter);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Prior reverted/failed decisions similar to `text`, most-similar first.
|
|
41
|
+
* @param {object[]} decisions
|
|
42
|
+
* @param {string} text
|
|
43
|
+
* @param {object} [opts] { project, minSimilarity=0.5 }
|
|
44
|
+
*/
|
|
45
|
+
function recallSimilar(decisions, text, { project = null, minSimilarity = 0.5 } = {}) {
|
|
46
|
+
return (decisions || [])
|
|
47
|
+
.filter((d) => d && REGRET.has(d.outcome) && (!project || d.project === project))
|
|
48
|
+
.map((d) => ({ ...d, similarity: similarity(text, d.summary) }))
|
|
49
|
+
.filter((d) => d.similarity >= minSimilarity)
|
|
50
|
+
.sort((a, b) => b.similarity - a.similarity);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The single closest prior regret, or null. */
|
|
54
|
+
function closestRegret(decisions, text, opts = {}) {
|
|
55
|
+
const hits = recallSimilar(decisions, text, opts);
|
|
56
|
+
return hits.length ? hits[0] : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { similarity, recallSimilar, closestRegret, tokens };
|