phewsh 0.15.10 → 0.15.12

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.
@@ -1,5 +1,6 @@
1
1
  // phewsh sequence (phewsh seq)
2
- // Universal Memory Transform — reads all AI memory files, emits optimal context.
2
+ // Universal Memory Transform — reads this directory's memory files plus the
3
+ // user's global per-tool memory (read-only), emits optimal context per target.
3
4
 
4
5
  const fs = require('fs');
5
6
  const path = require('path');
@@ -17,6 +18,7 @@ const flags = {
17
18
  write: args.includes('--write') || args.includes('-w'),
18
19
  dryRun: args.includes('--dry-run'),
19
20
  all: args.includes('--all'),
21
+ includeGlobal: args.includes('--include-global'),
20
22
  sources: getFlag('--sources', '-s'),
21
23
  help: args.includes('--help') || args.includes('-h'),
22
24
  };
@@ -47,15 +49,22 @@ function getPositionalTarget() {
47
49
  function showHelp() {
48
50
  console.log('');
49
51
  console.log(` ${b(cream('phewsh sequence'))} ${slate('(phewsh seq)')}`);
50
- console.log(` ${sage('Universal Memory Transform — reads all AI memory files,')}`);
51
- console.log(` ${sage('emits optimal context for any target agent.')}`);
52
+ console.log(` ${sage('Universal Memory Transform — reads the memory files in this')}`);
53
+ console.log(` ${sage('directory plus your global per-user memory across tools,')}`);
54
+ console.log(` ${sage('then emits optimal context for any target agent.')}`);
55
+ console.log('');
56
+ console.log(` ${cream('reads')} ${slate('(read-only — phewsh never edits these)')}`);
57
+ console.log(` ${sage('project .intent/, CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules,')}`);
58
+ console.log(` ${sage(' copilot-instructions, README, + this project’s Claude memory')}`);
59
+ console.log(` ${sage('global ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md')}`);
60
+ console.log(` ${slate('global memory is per-user and travels across every project.')}`);
52
61
  console.log('');
53
62
  console.log(` ${cream('usage')}`);
54
- console.log(` ${teal('phewsh seq')} ${sage('Sequence → stdout summary')}`);
55
- console.log(` ${teal('phewsh seq')} ${slate('claude')} ${sage('Sequence → CLAUDE.md section')}`);
63
+ console.log(` ${teal('phewsh seq')} ${sage('Sequence → stdout summary (project + global)')}`);
64
+ console.log(` ${teal('phewsh seq')} ${slate('claude')} ${sage('Sequence → CLAUDE.md section (project only)')}`);
56
65
  console.log(` ${teal('phewsh seq')} ${slate('-w')} ${sage('Write to target file')}`);
57
66
  console.log(` ${teal('phewsh seq')} ${slate('--explain')} ${sage('Show full ranking breakdown')}`);
58
- console.log(` ${teal('phewsh seq')} ${slate('--dry-run')} ${sage('Show sources found, no output')}`);
67
+ console.log(` ${teal('phewsh seq')} ${slate('--dry-run')} ${sage('Show sources found (with scope), no output')}`);
59
68
  console.log('');
60
69
  console.log(` ${cream('targets')}`);
61
70
  console.log(` ${teal('claude')} ${sage('CLAUDE.md section (between markers)')}`);
@@ -63,6 +72,8 @@ function showHelp() {
63
72
  console.log(` ${cream('options')}`);
64
73
  console.log(` ${teal('--budget')} ${slate('<level>')} ${sage('Token budget: minimal|standard|full|unlimited')}`);
65
74
  console.log(` ${teal('--sources')} ${slate('<list>')} ${sage('Limit sources: intent,claude-md,claude-memory')}`);
75
+ console.log(` ${teal('--include-global')} ${sage('Allow global memory into a written project file')}`);
76
+ console.log(` ${slate(' (off by default — keeps personal notes out of committed files)')}`);
66
77
  console.log(` ${teal('--write, -w')} ${sage('Write output to target file')}`);
67
78
  console.log(` ${teal('--explain, -e')} ${sage('Full ranking breakdown')}`);
68
79
  console.log(` ${teal('--dry-run')} ${sage('Discover sources only')}`);
@@ -82,6 +93,9 @@ async function main() {
82
93
  sources = sources.filter(s => sourceFilter.some(f => s.type === f || s.type.startsWith(f)));
83
94
  }
84
95
 
96
+ const projectCount = sources.filter(s => s.scope !== 'global').length;
97
+ const globalCount = sources.filter(s => s.scope === 'global').length;
98
+
85
99
  console.log('');
86
100
  console.log(` ${b(cream('Sources discovered'))} ${slate(`(${sources.length})`)}`);
87
101
  ui.divider('line');
@@ -89,10 +103,13 @@ async function main() {
89
103
  console.log(` ${sage('No recognized memory files found in')} ${slate(process.cwd())}`);
90
104
  } else {
91
105
  for (const source of sources) {
92
- console.log(` ${teal(source.type.padEnd(20))} ${sage(source.name)}`);
106
+ const tag = source.scope === 'global' ? slate('global ') : sage('project');
107
+ console.log(` ${tag} ${teal(source.type.padEnd(18))} ${sage(source.name)}`);
93
108
  }
94
109
  }
95
110
  ui.divider('line');
111
+ console.log(` ${slate(`${projectCount} project · ${globalCount} global`)}`);
112
+ console.log(` ${slate('global = per-user memory across all tools; summary-only unless --include-global on write')}`);
96
113
  console.log('');
97
114
  return;
98
115
  }
@@ -107,6 +124,7 @@ async function main() {
107
124
  sources: sourceFilter,
108
125
  explain: flags.explain,
109
126
  write: flags.write,
127
+ includeGlobal: flags.includeGlobal,
110
128
  });
111
129
 
112
130
  // If target is stdout, emit already printed
@@ -19,6 +19,7 @@ const { readPPS } = require('../lib/pps');
19
19
  const { push, pull, ensureValidToken } = require('./sync');
20
20
  const { HARNESSES, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
21
21
  const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
22
+ const { suggest, suggestAll } = require('../lib/suggest');
22
23
  const { recordSessionEvent } = require('../lib/receipts-data');
23
24
  const configFile = require('../lib/config-file');
24
25
  const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
@@ -330,6 +331,7 @@ async function main() {
330
331
  let awaitingOutcome = null; // decision id eligible for 1-4 labeling
331
332
  let awaitingFallback = null; // { input, fullSystem, options } after a route failure
332
333
  let bootstrapChoices = null; // root-bootstrap menu entries when no project here
334
+ let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
333
335
  let decisionsThisSession = 0;
334
336
 
335
337
  // ── The Exhale: animated brand reveal ──────────────────
@@ -454,6 +456,55 @@ async function main() {
454
456
  console.log(` ${slate('pick a number, or just type — your context travels with every route')}`);
455
457
  }
456
458
 
459
+ // Self-aware guidance: snapshot the session's state so phewsh can recommend
460
+ // the one next step worth taking, instead of leaving the user to know commands.
461
+ function buildSuggestState() {
462
+ let seqStale = false;
463
+ try {
464
+ const cwd = process.cwd();
465
+ const claudePath = path.join(cwd, 'CLAUDE.md');
466
+ const intentDir = path.join(cwd, '.intent');
467
+ if (fs.existsSync(claudePath) && fs.existsSync(intentDir)) {
468
+ const claudeT = fs.statSync(claudePath).mtimeMs;
469
+ const newestIntent = fs.readdirSync(intentDir)
470
+ .filter(f => f.endsWith('.md') || f.endsWith('.json'))
471
+ .reduce((m, f) => Math.max(m, fs.statSync(path.join(intentDir, f)).mtimeMs), 0);
472
+ seqStale = newestIntent > claudeT + 1000; // >1s newer = drift
473
+ }
474
+ } catch { /* drift detection is best-effort */ }
475
+
476
+ let ambientOn = false;
477
+ try {
478
+ const s = fs.readFileSync(path.join(os.homedir(), '.claude', 'settings.json'), 'utf-8');
479
+ ambientOn = s.includes('phewsh hook session-start');
480
+ } catch { /* no settings = ambient off */ }
481
+
482
+ let pending = 0;
483
+ try { pending = pendingDecisions({ project: projectName }).length; } catch { /* best-effort */ }
484
+
485
+ let installed = [];
486
+ try { installed = listHarnesses().filter(h => h.installed).map(h => h.id); } catch { /* best-effort */ }
487
+
488
+ return {
489
+ hasIntentDir: fs.existsSync(path.join(process.cwd(), '.intent')),
490
+ intentFileCount: intentFiles.length,
491
+ pendingOutcomes: pending,
492
+ installedHarnesses: installed,
493
+ route,
494
+ turnsThisSession: Math.floor(messages.length / 2),
495
+ seqStale,
496
+ ambientOn,
497
+ };
498
+ }
499
+
500
+ // One subtle line under the menu — the single highest-leverage nudge, if any.
501
+ function showInlineTip() {
502
+ let tip = null;
503
+ try { tip = suggest(buildSuggestState()); } catch { /* never block the prompt on guidance */ }
504
+ if (!tip) return;
505
+ console.log(` ${teal('⤷')} ${sage(tip.message)} ${cream(tip.command.trim())} ${slate('· /next for options')}`);
506
+ }
507
+
457
508
  // Open a known project from the bootstrap menu: chdir, reload memory,
458
509
  // back to the normal flow. The session is the cockpit; projects swap in.
459
510
  function openProjectAt(dir) {
@@ -470,6 +521,7 @@ async function main() {
470
521
  console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(`.intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''} loaded`)} ${slate('· via ' + routeLabel(route, config))}`);
471
522
  console.log('');
472
523
  showModeMenu();
524
+ showInlineTip();
473
525
  console.log('');
474
526
  }
475
527
 
@@ -502,6 +554,7 @@ async function main() {
502
554
  showBootstrapMenu(recents);
503
555
  } else {
504
556
  showModeMenu();
557
+ showInlineTip();
505
558
  }
506
559
  console.log('');
507
560
 
@@ -696,7 +749,8 @@ async function main() {
696
749
  'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'run',
697
750
  'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
698
751
  'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
699
- 'agents', 'context', 'gate', 'reload', 'sequence', 'setup', 'system', 'watch',
752
+ 'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
753
+ 'next', 'recommend', 'guide',
700
754
  ]);
701
755
  const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
702
756
  let turnAbort = null; // AbortController while an API turn streams
@@ -892,6 +946,29 @@ async function main() {
892
946
  return;
893
947
  }
894
948
 
949
+ // /next pick: a bare number runs the chosen suggestion (slash = in place).
950
+ // Any non-digit input means the user moved on — drop the offer so it never
951
+ // shadows a later mode pick.
952
+ if (nextChoices && !/^[0-9]$/.test(input)) nextChoices = null;
953
+ if (nextChoices && /^[0-9]$/.test(input)) {
954
+ const pick = nextChoices[parseInt(input, 10) - 1];
955
+ if (!pick) {
956
+ console.log(` ${sage('Pick 1-' + nextChoices.length + ', or keep typing')}`);
957
+ rl.prompt();
958
+ return;
959
+ }
960
+ const command = pick.command.trim();
961
+ nextChoices = null;
962
+ if (command.startsWith('/')) {
963
+ await handleInput(command); // re-dispatch the slash command in place
964
+ return;
965
+ }
966
+ console.log(` ${teal('⤷')} ${sage('run this in your shell:')} ${cream(command)}`);
967
+ console.log('');
968
+ rl.prompt();
969
+ return;
970
+ }
971
+
895
972
  // Root bootstrap: a bare number opens a project, inits, or scans
896
973
  if (bootstrapChoices && messages.length === 0 && /^[0-9]{1,2}$/.test(input)) {
897
974
  const choice = bootstrapChoices[parseInt(input, 10) - 1];
@@ -996,10 +1073,41 @@ async function main() {
996
1073
  process.exit(0);
997
1074
  }
998
1075
 
1076
+ // ── /next ──────────────────────────────────────────
1077
+ // The self-aware "what should I do?" button: phewsh reads the session's
1078
+ // state and hands back ranked, pickable next steps — no command to recall.
1079
+ if (cmd === 'next' || cmd === 'recommend' || cmd === 'guide') {
1080
+ let ranked = [];
1081
+ try { ranked = suggestAll(buildSuggestState()); } catch { /* best-effort */ }
1082
+ console.log('');
1083
+ if (ranked.length === 0) {
1084
+ console.log(` ${teal('●')} ${sage("You're aligned — nothing pressing. Keep working, or /help for everything.")}`);
1085
+ console.log('');
1086
+ nextChoices = null;
1087
+ rl.prompt();
1088
+ return;
1089
+ }
1090
+ console.log(` ${b(cream('What would move you forward'))} ${slate('— pick a number, or ignore me')}`);
1091
+ console.log('');
1092
+ nextChoices = ranked.slice(0, 3);
1093
+ nextChoices.forEach((s, i) => {
1094
+ console.log(` ${teal(String(i + 1))} ${cream(s.command.trim())} ${sage(s.message)}`);
1095
+ console.log(` ${slate(s.why)}`);
1096
+ });
1097
+ console.log('');
1098
+ console.log(` ${slate('a slash command runs in place; anything else, I show you the line to run')}`);
1099
+ console.log('');
1100
+ rl.prompt();
1101
+ return;
1102
+ }
1103
+
999
1104
  if (cmd === 'help' || cmd === 'h') {
1000
1105
  console.log('');
1001
1106
  console.log(` ${sage('the loop: define .intent/ → sync → work → evolve → repeat')}`);
1002
1107
  console.log('');
1108
+ console.log(` ${cream('not sure what to do?')}`);
1109
+ console.log(` ${teal('/next')} ${sage("phewsh reads your state and hands back the next step worth taking")}`);
1110
+ console.log('');
1003
1111
  console.log(` ${cream('author .intent/')}`);
1004
1112
  console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
1005
1113
  console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
@@ -1,5 +1,15 @@
1
- // Discover all memory/context source files in the working directory.
2
- // Returns a list of { path, type } for each recognized source.
1
+ // Discover all memory/context source files for the working directory.
2
+ // Returns a list of { path, type, name, scope } for each recognized source.
3
+ //
4
+ // Two scopes:
5
+ // 'project' — files in the working directory (this repo/project)
6
+ // 'global' — per-user memory that travels across every project
7
+ // (your global CLAUDE.md, Codex AGENTS.md, Gemini GEMINI.md)
8
+ //
9
+ // Global sources enrich the summary so `phewsh seq` reflects cross-tool
10
+ // continuity even in a bare directory. They are deliberately kept OUT of
11
+ // project-file writes by default (see sequence() includeGlobal) so personal
12
+ // global notes never leak into a committed project CLAUDE.md.
3
13
 
4
14
  const fs = require('fs');
5
15
  const path = require('path');
@@ -8,8 +18,18 @@ const os = require('os');
8
18
  const INTENT_FILES = ['vision.md', 'plan.md', 'status.md', 'narrative.md', 'next.md'];
9
19
  const INTENT_JSON = ['project.json', 'pps.json', 'gate.json'];
10
20
 
11
- function discover(cwd = process.cwd()) {
21
+ function discover(cwd = process.cwd(), home = os.homedir()) {
12
22
  const sources = [];
23
+ const seenPaths = new Set();
24
+
25
+ const add = (s) => {
26
+ const real = path.resolve(s.path);
27
+ if (seenPaths.has(real)) return; // never list the same file twice
28
+ seenPaths.add(real);
29
+ sources.push(s);
30
+ };
31
+
32
+ // ── Project scope ──────────────────────────────────────────────
13
33
 
14
34
  // .intent/ artifacts
15
35
  const intentDir = path.join(cwd, '.intent');
@@ -17,7 +37,7 @@ function discover(cwd = process.cwd()) {
17
37
  for (const file of [...INTENT_FILES, ...INTENT_JSON]) {
18
38
  const p = path.join(intentDir, file);
19
39
  if (fs.existsSync(p)) {
20
- sources.push({ path: p, type: 'intent', name: file });
40
+ add({ path: p, type: 'intent', name: file, scope: 'project' });
21
41
  }
22
42
  }
23
43
  }
@@ -25,12 +45,12 @@ function discover(cwd = process.cwd()) {
25
45
  // CLAUDE.md — split into manual and generated sections later by parser
26
46
  const claudeMd = path.join(cwd, 'CLAUDE.md');
27
47
  if (fs.existsSync(claudeMd)) {
28
- sources.push({ path: claudeMd, type: 'claude-md', name: 'CLAUDE.md' });
48
+ add({ path: claudeMd, type: 'claude-md', name: 'CLAUDE.md', scope: 'project' });
29
49
  }
30
50
 
31
51
  // Claude auto-memory — project-scoped
32
- // .claude/projects/<encoded-cwd>/memory/MEMORY.md
33
- const claudeDir = path.join(os.homedir(), '.claude');
52
+ // ~/.claude/projects/<encoded-cwd>/memory/MEMORY.md
53
+ const claudeDir = path.join(home, '.claude');
34
54
  if (fs.existsSync(claudeDir)) {
35
55
  const projectsDir = path.join(claudeDir, 'projects');
36
56
  if (fs.existsSync(projectsDir)) {
@@ -40,7 +60,7 @@ function discover(cwd = process.cwd()) {
40
60
  if (fs.existsSync(memoryDir)) {
41
61
  const memoryIndex = path.join(memoryDir, 'MEMORY.md');
42
62
  if (fs.existsSync(memoryIndex)) {
43
- sources.push({ path: memoryIndex, type: 'claude-memory', name: 'MEMORY.md' });
63
+ add({ path: memoryIndex, type: 'claude-memory', name: 'MEMORY.md', scope: 'project' });
44
64
 
45
65
  // Also discover linked memory files from MEMORY.md
46
66
  try {
@@ -50,7 +70,7 @@ function discover(cwd = process.cwd()) {
50
70
  while ((match = linkRegex.exec(content)) !== null) {
51
71
  const linked = path.join(memoryDir, match[2]);
52
72
  if (fs.existsSync(linked)) {
53
- sources.push({ path: linked, type: 'claude-memory-file', name: match[2] });
73
+ add({ path: linked, type: 'claude-memory-file', name: match[2], scope: 'project' });
54
74
  }
55
75
  }
56
76
  } catch { /* skip */ }
@@ -62,33 +82,54 @@ function discover(cwd = process.cwd()) {
62
82
  // .cursorrules
63
83
  const cursorrules = path.join(cwd, '.cursorrules');
64
84
  if (fs.existsSync(cursorrules)) {
65
- sources.push({ path: cursorrules, type: 'cursor', name: '.cursorrules' });
85
+ add({ path: cursorrules, type: 'cursor', name: '.cursorrules', scope: 'project' });
66
86
  }
67
87
 
68
- // agent.md / AGENTS.md
88
+ // agent.md / AGENTS.md (Codex / generic agent instructions, project scope)
69
89
  for (const name of ['agent.md', 'AGENTS.md']) {
70
90
  const p = path.join(cwd, name);
71
91
  if (fs.existsSync(p)) {
72
- sources.push({ path: p, type: 'agent', name });
92
+ add({ path: p, type: 'agent', name, scope: 'project' });
73
93
  }
74
94
  }
75
95
 
96
+ // GEMINI.md (project scope)
97
+ const geminiMd = path.join(cwd, 'GEMINI.md');
98
+ if (fs.existsSync(geminiMd)) {
99
+ add({ path: geminiMd, type: 'agent', name: 'GEMINI.md', scope: 'project' });
100
+ }
101
+
76
102
  // soul.md
77
103
  const soulMd = path.join(cwd, 'soul.md');
78
104
  if (fs.existsSync(soulMd)) {
79
- sources.push({ path: soulMd, type: 'soul', name: 'soul.md' });
105
+ add({ path: soulMd, type: 'soul', name: 'soul.md', scope: 'project' });
80
106
  }
81
107
 
82
108
  // .github/copilot-instructions.md
83
109
  const copilot = path.join(cwd, '.github', 'copilot-instructions.md');
84
110
  if (fs.existsSync(copilot)) {
85
- sources.push({ path: copilot, type: 'copilot', name: 'copilot-instructions.md' });
111
+ add({ path: copilot, type: 'copilot', name: 'copilot-instructions.md', scope: 'project' });
86
112
  }
87
113
 
88
114
  // README.md (low priority but useful for identity)
89
115
  const readme = path.join(cwd, 'README.md');
90
116
  if (fs.existsSync(readme)) {
91
- sources.push({ path: readme, type: 'readme', name: 'README.md' });
117
+ add({ path: readme, type: 'readme', name: 'README.md', scope: 'project' });
118
+ }
119
+
120
+ // ── Global scope (per-user, travels across every project) ──────
121
+ // Canonical global memory files for each agent CLI. Read-only; never
122
+ // written to. These give value even with no .intent/ in the directory.
123
+ // De-dup via seenPaths handles the edge where cwd === home.
124
+ const globals = [
125
+ { path: path.join(home, '.claude', 'CLAUDE.md'), type: 'claude-md', name: '~/.claude/CLAUDE.md' },
126
+ { path: path.join(home, '.codex', 'AGENTS.md'), type: 'agent', name: '~/.codex/AGENTS.md' },
127
+ { path: path.join(home, '.gemini', 'GEMINI.md'), type: 'agent', name: '~/.gemini/GEMINI.md' },
128
+ ];
129
+ for (const g of globals) {
130
+ if (fs.existsSync(g.path)) {
131
+ add({ path: g.path, type: g.type, name: g.name, scope: 'global' });
132
+ }
92
133
  }
93
134
 
94
135
  return sources;
@@ -58,7 +58,8 @@ function emitExplain(chunks, sources, { b, w, g, sage, slate, teal, cream, green
58
58
  // Sources discovered
59
59
  console.log(` ${b(cream('Sources'))}`);
60
60
  for (const source of sources) {
61
- console.log(` ${teal(source.type.padEnd(16))} ${sage(source.name)}`);
61
+ const tag = source.scope === 'global' ? slate('global ') : sage('project');
62
+ console.log(` ${tag} ${teal(source.type.padEnd(14))} ${sage(source.name)}`);
62
63
  }
63
64
  console.log('');
64
65
 
@@ -45,12 +45,20 @@ function sequence(options = {}) {
45
45
  sources: sourceFilter = null,
46
46
  explain = false,
47
47
  write = false,
48
+ includeGlobal = false,
48
49
  cwd = process.cwd(),
49
50
  } = options;
50
51
 
51
- // 1. Discover all source files
52
+ // 1. Discover all source files (project + global)
52
53
  let sources = discover(cwd);
53
54
 
55
+ // Privacy guard: global per-user memory enriches the summary (stdout), but
56
+ // never bleeds into a project-file target (CLAUDE.md) unless asked for —
57
+ // so personal global notes don't land in a committed project file.
58
+ if (target !== 'stdout' && !includeGlobal) {
59
+ sources = sources.filter(s => s.scope !== 'global');
60
+ }
61
+
54
62
  // Filter sources if requested
55
63
  if (sourceFilter && sourceFilter.length > 0) {
56
64
  sources = sources.filter(s =>
@@ -39,8 +39,9 @@ function parse(source) {
39
39
 
40
40
  const kind = classifySection(section.title);
41
41
  chunks.push({
42
- source: `CLAUDE.md:${section.title || 'root'}`,
42
+ source: `${source.name}:${section.title || 'root'}`,
43
43
  sourceType: 'claude-md-manual',
44
+ scope: source.scope || 'project',
44
45
  kind,
45
46
  content: section.body.trim(),
46
47
  timestamp: mtime,
@@ -52,8 +53,9 @@ function parse(source) {
52
53
  // Generated section — lower authority, will get deduped against .intent/
53
54
  if (generatedContent) {
54
55
  chunks.push({
55
- source: 'CLAUDE.md:phewsh-generated',
56
+ source: `${source.name}:phewsh-generated`,
56
57
  sourceType: 'claude-md-generated',
58
+ scope: source.scope || 'project',
57
59
  kind: 'context',
58
60
  content: generatedContent,
59
61
  timestamp: mtime,
@@ -21,6 +21,7 @@ function parse(source) {
21
21
  return [{
22
22
  source: source.name,
23
23
  sourceType: source.type,
24
+ scope: source.scope || 'project',
24
25
  kind,
25
26
  content: content.trim(),
26
27
  timestamp: mtime,
package/lib/suggest.js ADDED
@@ -0,0 +1,111 @@
1
+ // Self-aware guidance — phewsh watching the user's state and surfacing the
2
+ // single most useful next step, so nobody has to memorize commands like
3
+ // `phewsh seq --dry-run`. Pure and deterministic: feed it a state snapshot,
4
+ // get back ranked suggestions. The session layer renders + offers them.
5
+ //
6
+ // Each suggestion: { id, priority, message, command, why }
7
+ // message — one plain line, what's true right now (the caller colorizes)
8
+ // command — the exact thing to run (a slash command or shell command)
9
+ // why — one short clause: why it helps (shown on /next, not inline)
10
+ //
11
+ // Design rules:
12
+ // - Never nag: a suggestion only fires when its trigger is genuinely met.
13
+ // - One at a time inline; up to a few on demand via /next.
14
+ // - Ordered by leverage — capture-intent and learn-from-outcomes first,
15
+ // because those are the moat; convenience nudges last.
16
+
17
+ /**
18
+ * @param {object} s state snapshot
19
+ * @param {boolean} s.hasIntentDir — .intent/ exists in cwd
20
+ * @param {number} s.intentFileCount — .intent/ files loaded
21
+ * @param {number} s.pendingOutcomes — unlabeled decisions (this project)
22
+ * @param {string[]} s.installedHarnesses — harness ids present on the machine
23
+ * @param {string[]} [s.usedRoutes] — harness ids used this session
24
+ * @param {string|null} s.route — current route id
25
+ * @param {number} s.turnsThisSession — exchanges so far
26
+ * @param {boolean} s.seqStale — .intent/ newer than CLAUDE.md (drift)
27
+ * @param {boolean} s.ambientOn — ambient hooks installed
28
+ * @returns {Array} ranked suggestions (highest leverage first)
29
+ */
30
+ function suggestAll(s = {}) {
31
+ const {
32
+ hasIntentDir = false,
33
+ intentFileCount = 0,
34
+ pendingOutcomes = 0,
35
+ installedHarnesses = [],
36
+ usedRoutes = [],
37
+ route = null,
38
+ turnsThisSession = 0,
39
+ seqStale = false,
40
+ ambientOn = false,
41
+ } = s;
42
+
43
+ const out = [];
44
+
45
+ // 1. Working without captured intent — the biggest miss. Every AI tool here
46
+ // could be reading the same goal; right now none are.
47
+ if (intentFileCount === 0 && turnsThisSession >= 2) {
48
+ out.push({
49
+ id: 'capture-intent',
50
+ priority: 100,
51
+ message: `${turnsThisSession} messages in, no captured intent — every AI here is guessing.`,
52
+ command: '/clarify',
53
+ why: 'Turns what you just said into a spec every tool (this one, Claude Code, Codex) reads.',
54
+ });
55
+ }
56
+
57
+ // 2. Decisions piling up unlabeled — the record only gets smarter if labeled.
58
+ if (pendingOutcomes >= 3) {
59
+ out.push({
60
+ id: 'label-outcomes',
61
+ priority: 90,
62
+ message: `${pendingOutcomes} decisions unlabeled — phewsh can't learn what you keep until you say.`,
63
+ command: '/outcomes',
64
+ why: 'Labeling kept/reverted is the dataset; it weights future routing and recall.',
65
+ });
66
+ }
67
+
68
+ // 3. .intent/ drifted from CLAUDE.md — the editor's AI is reading stale context.
69
+ if (hasIntentDir && seqStale) {
70
+ out.push({
71
+ id: 'resync-claude-md',
72
+ priority: 80,
73
+ message: `Your .intent/ changed but CLAUDE.md didn't — editor AIs are reading stale context.`,
74
+ command: '/seq claude',
75
+ why: 'Re-sequences the latest intent + memory into CLAUDE.md so every tool sees today.',
76
+ });
77
+ }
78
+
79
+ // 4. Multiple harnesses installed, only leaning on one — council is free leverage.
80
+ const others = installedHarnesses.filter(h => h !== route);
81
+ if (installedHarnesses.length >= 2 && turnsThisSession >= 3 && others.length >= 1) {
82
+ out.push({
83
+ id: 'try-council',
84
+ priority: 60,
85
+ message: `You have ${installedHarnesses.length} agents installed but lean on one — the rest are idle.`,
86
+ command: '/council ',
87
+ why: 'Asks every installed harness in parallel; you keep the best answer, context shared.',
88
+ });
89
+ }
90
+
91
+ // 5. Ambient off — continuity that costs nothing once turned on.
92
+ if (hasIntentDir && !ambientOn && installedHarnesses.includes('claude-code')) {
93
+ out.push({
94
+ id: 'enable-ambient',
95
+ priority: 50,
96
+ message: `Ambient is off — Claude Code sessions here won't get the brief unless you launch phewsh.`,
97
+ command: 'phewsh ambient',
98
+ why: 'Installs a silent SessionStart hook so continuity travels even when you forget phewsh.',
99
+ });
100
+ }
101
+
102
+ return out.sort((a, b) => b.priority - a.priority);
103
+ }
104
+
105
+ /** The single highest-leverage suggestion, or null. */
106
+ function suggest(s = {}) {
107
+ const all = suggestAll(s);
108
+ return all.length ? all[0] : null;
109
+ }
110
+
111
+ module.exports = { suggest, suggestAll };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.10",
3
+ "version": "0.15.12",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"