phewsh 0.15.24 → 0.15.25

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.
@@ -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');
@@ -583,7 +584,25 @@ async function main() {
583
584
  const failureTracker = createFailureTracker();
584
585
  let lastTurnFailure = null;
585
586
 
587
+ // The gate looking backward: if this is close to something you already
588
+ // reverted or failed, say so once — quietly, before the turn runs.
589
+ let lastRecallId = null;
590
+ function recallHeadsUp(input) {
591
+ try {
592
+ const past = recentDecisions(300, { project: projectName });
593
+ const hit = recall.closestRegret(past, input, { project: projectName, minSimilarity: 0.5 });
594
+ if (!hit || hit.id === lastRecallId) return;
595
+ lastRecallId = hit.id;
596
+ let s = (hit.summary || '').replace(/\s+/g, ' ');
597
+ if (s.length > 50) s = s.slice(0, 49).trimEnd() + '…';
598
+ const verb = hit.outcome === 'failed' ? 'failed' : 'reverted';
599
+ console.log(` ${peach('↩')} ${sage(`You ${verb} something close before:`)} ${slate('“' + s + '” · via ' + continuity.labelFor(hit.route) + ' · ' + continuity.agoText(hit.ts))}`);
600
+ console.log(` ${slate(' not a block — just so the record doesn\'t let you repeat it blind.')}`);
601
+ } catch { /* recall is advisory, never blocks a turn */ }
602
+ }
603
+
586
604
  async function runHarnessTurn(input, harnessId, fullSystem) {
605
+ recallHeadsUp(input);
587
606
  const decisionId = recordDecision({
588
607
  project: projectName, route: harnessId, mode: sessionMode, summary: input,
589
608
  });
@@ -623,6 +642,7 @@ async function main() {
623
642
  }
624
643
 
625
644
  async function runApiTurn(input, fullSystem) {
645
+ recallHeadsUp(input);
626
646
  const decisionId = recordDecision({
627
647
  project: projectName, route: 'api', mode: sessionMode, summary: input,
628
648
  });
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.24",
3
+ "version": "0.15.25",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"