phewsh 0.15.42 → 0.15.44

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.
@@ -19,6 +19,7 @@ const os = require('os');
19
19
  const readline = require('readline');
20
20
  const { listHarnesses } = require('../lib/harnesses');
21
21
  const selfheal = require('../lib/selfheal');
22
+ const slash = require('../lib/slash-commands');
22
23
 
23
24
  const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
24
25
  const LEDGER_FILE = path.join(PHEWSH_DIR, 'ambient.json');
@@ -169,6 +170,7 @@ async function turnOn(skipConfirm) {
169
170
 
170
171
  const changes = applyClaudeHooks();
171
172
  const { written } = selfheal.syncGlobalBaseFiles();
173
+ const slashWritten = slash.installSlashCommands().written;
172
174
  const ledger = loadLedger();
173
175
  ledger.applied['claude-code'] = {
174
176
  at: new Date().toISOString(),
@@ -184,6 +186,13 @@ async function turnOn(skipConfirm) {
184
186
  undo: 'phewsh ambient off',
185
187
  };
186
188
  }
189
+ if (slashWritten.length > 0 || ledger.applied.slashCommands) {
190
+ ledger.applied.slashCommands = {
191
+ at: new Date().toISOString(),
192
+ tools: slashWritten.length > 0 ? slashWritten : (ledger.applied.slashCommands || {}).tools || [],
193
+ undo: 'phewsh ambient off',
194
+ };
195
+ }
187
196
  ledger.seenHarnesses = harnesses.filter(h => h.installed).map(h => h.id);
188
197
  ledger.disabled = false; // explicit enable clears any prior opt-out
189
198
  saveLedger(ledger);
@@ -192,6 +201,7 @@ async function turnOn(skipConfirm) {
192
201
  console.log(` ${green('●')} ${b('Ambient is on.')}`);
193
202
  changes.forEach(c => console.log(` ${teal('+')} ${slate(c)}`));
194
203
  written.forEach(f => console.log(` ${teal('+')} ${slate('base file ' + f + ' (machine-wide phewsh awareness)')}`));
204
+ if (slashWritten.length) console.log(` ${teal('+')} ${cream('/intent')} ${slate('command added to ' + slashWritten.join(', '))}`);
195
205
  console.log('');
196
206
  console.log(` ${sage('Next Claude Code session in a project with')} ${cream('.intent/')} ${sage('starts pre-briefed.')}`);
197
207
  if (written.length > 0) {
@@ -204,17 +214,20 @@ async function turnOn(skipConfirm) {
204
214
  function turnOff() {
205
215
  const removed = removeClaudeHooks();
206
216
  const { removed: removedGlobal } = selfheal.removeGlobalBaseFiles();
217
+ const { removed: removedSlash } = slash.removeSlashCommands();
207
218
  const ledger = loadLedger();
208
219
  delete ledger.applied['claude-code'];
209
220
  delete ledger.applied.globalBase;
221
+ delete ledger.applied.slashCommands;
210
222
  delete ledger.autoEnabledAt;
211
223
  ledger.disabled = true; // sticky opt-out — first-run auto-enable must respect this
212
224
  saveLedger(ledger);
213
225
  console.log('');
214
- if (removed.length > 0 || removedGlobal.length > 0) {
226
+ if (removed.length > 0 || removedGlobal.length > 0 || removedSlash.length > 0) {
215
227
  console.log(` ${green('●')} ${b('Ambient is off.')}`);
216
228
  removed.forEach(c => console.log(` ${peach('-')} ${slate(c)}`));
217
229
  removedGlobal.forEach(f => console.log(` ${peach('-')} ${slate('base file ' + f + ' (phewsh block removed)')}`));
230
+ if (removedSlash.length) console.log(` ${peach('-')} ${slate('/intent command removed from ' + removedSlash.join(', '))}`);
218
231
  console.log(` ${sage('Breadcrumb log kept at')} ${cream('~/.phewsh/ambient-sessions.jsonl')} ${sage('— delete it if you want; phewsh never will.')}`);
219
232
  } else {
220
233
  console.log(` ${sage('Nothing was applied — nothing to remove.')}`);
@@ -302,6 +315,7 @@ async function ensureAuto() {
302
315
  const hasClaude = installed.some(h => h.id === 'claude-code');
303
316
  const changes = hasClaude ? applyClaudeHooks() : [];
304
317
  const { written } = selfheal.syncGlobalBaseFiles();
318
+ const slashWritten = slash.installSlashCommands().written;
305
319
  // Refresh existing project files only — first-run auto-enable must not dump
306
320
  // context files into whatever repo the user happens to be standing in.
307
321
  try { selfheal.syncContextFiles({ createMissing: false }); } catch { /* best-effort */ }
@@ -317,6 +331,9 @@ async function ensureAuto() {
317
331
  if (written.length > 0) {
318
332
  ledger.applied.globalBase = { at: now, files: written, undo: 'phewsh ambient off' };
319
333
  }
334
+ if (slashWritten.length > 0) {
335
+ ledger.applied.slashCommands = { at: now, tools: slashWritten, undo: 'phewsh ambient off' };
336
+ }
320
337
  ledger.disabled = false;
321
338
  ledger.autoEnabledAt = now;
322
339
  ledger.seenHarnesses = installed.map(h => h.id);
@@ -327,6 +344,7 @@ async function ensureAuto() {
327
344
  console.log(` ${b(cream('phewsh set itself up across your AI tools'))} ${sage('— so they stay in sync with your project intent.')}`);
328
345
  if (hasClaude) console.log(` ${teal('+')} ${slate('Claude Code gets a brief from a project\'s .intent/ at session start')}`);
329
346
  written.forEach(f => console.log(` ${teal('+')} ${slate('environment note added to ' + f)}`));
347
+ if (slashWritten.length) console.log(` ${teal('+')} ${cream('/intent')} ${slate('command added to ' + slashWritten.join(', '))}`);
330
348
  console.log(` ${sage('In any project with')} ${cream('.intent/')}${sage(', your tools now read its real intent. Reversible:')} ${cream('phewsh ambient off')}`);
331
349
  console.log(` ${sage('Want a guaranteed status line each time you launch a tool?')} ${cream('phewsh shim on')}`);
332
350
  console.log('');
@@ -579,6 +579,9 @@ async function main() {
579
579
  ambientOn = s.includes('phewsh hook session-start');
580
580
  } catch { /* no settings = ambient off */ }
581
581
 
582
+ let shimOn = false;
583
+ try { shimOn = require('../lib/shims').shimStatus().installed.length > 0; } catch { /* shims off */ }
584
+
582
585
  let pending = 0;
583
586
  // Only substantive calls — never nag the user to "judge" a greeting.
584
587
  try { pending = pendingDecisions({ project: projectName, substantive: true }).length; } catch { /* best-effort */ }
@@ -601,6 +604,7 @@ async function main() {
601
604
  turnsThisSession: Math.floor(messages.length / 2),
602
605
  seqStale,
603
606
  ambientOn,
607
+ shimOn,
604
608
  commitsSinceIntent: (() => { try { return selfheal.commitsSinceIntent(); } catch { return 0; } })(),
605
609
  bestKeeper,
606
610
  };
package/lib/shims.js CHANGED
@@ -65,24 +65,79 @@ exit 127
65
65
  `;
66
66
  }
67
67
 
68
- // Deterministic launch banner computed by phewsh, offline, fast. This is the
69
- // proof-of-life the user asked for: it appears because phewsh printed it.
68
+ // First meaningful line of a markdown file past YAML frontmatter and headings.
69
+ function firstLine(file, max = 64) {
70
+ try {
71
+ let body = fs.readFileSync(file, 'utf-8');
72
+ if (body.startsWith('---')) { const e = body.indexOf('\n---', 3); if (e !== -1) body = body.slice(e + 4); }
73
+ const line = body.split('\n').map(l => l.trim())
74
+ .find(l => l && !l.startsWith('#') && !l.startsWith('>') && !l.startsWith('---'));
75
+ if (!line) return null;
76
+ const clean = line.replace(/[*_`]/g, '').replace(/\s+/g, ' ').trim();
77
+ return clean.length > max ? clean.slice(0, max - 1) + '…' : clean;
78
+ } catch { return null; }
79
+ }
80
+
81
+ // How many decisions phewsh has recorded for this project (cwd basename key).
82
+ function decisionCount(cwd) {
83
+ try {
84
+ const file = path.join(PHEWSH_DIR, 'outcomes', 'decisions.json');
85
+ const all = JSON.parse(fs.readFileSync(file, 'utf-8'));
86
+ const key = path.basename(cwd);
87
+ const list = Array.isArray(all) ? all : (all.decisions || []);
88
+ return list.filter(d => (d.project || d.cwd && path.basename(d.cwd)) === key).length;
89
+ } catch { return 0; }
90
+ }
91
+
92
+ // Per-repo nudge counter — so the "create intent" coaching shows for the first
93
+ // few launches then goes quiet (a coach, not a billboard). Best-effort.
94
+ function bumpNudge(cwd) {
95
+ try {
96
+ const file = path.join(PHEWSH_DIR, 'shim-seen.json');
97
+ let seen = {};
98
+ try { seen = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch { /* fresh */ }
99
+ seen[cwd] = (seen[cwd] || 0) + 1;
100
+ fs.mkdirSync(PHEWSH_DIR, { recursive: true });
101
+ fs.writeFileSync(file, JSON.stringify(seen));
102
+ return seen[cwd];
103
+ } catch { return 99; } // on failure, assume "seen" so we don't nag
104
+ }
105
+
106
+ // Deterministic launch banner — computed by phewsh, offline, fast. Evidence +
107
+ // VALUE + GUIDANCE, not just "active". phewsh prints it, so it's guaranteed.
108
+ const C = { teal: s => `\x1b[38;5;79m${s}\x1b[0m`, sage: s => `\x1b[38;5;151m${s}\x1b[0m`, slate: s => `\x1b[38;5;247m${s}\x1b[0m`, cream: s => `\x1b[38;5;230m${s}\x1b[0m`, peach: s => `\x1b[38;5;216m${s}\x1b[0m` };
70
109
  function preflightBanner(bin, cwd = process.cwd()) {
71
- let intent = 'no .intent here';
72
- let record = '';
73
110
  try {
74
111
  const intentDir = path.join(cwd, '.intent');
75
- if (fs.existsSync(intentDir)) {
76
- const n = fs.readdirSync(intentDir).filter(f => /\.(md|json)$/.test(f)).length;
77
- intent = `.intent loaded (${n} file${n === 1 ? '' : 's'})`;
78
- try {
79
- const { statusDrift } = require('./truth');
80
- const d = statusDrift(cwd);
81
- if (d && d.tracked) record = d.commitsSince > 0 ? ` · record ${d.commitsSince} behind` : ' · record current';
82
- } catch { /* drift is a nicety */ }
112
+ if (!fs.existsSync(intentDir)) {
113
+ // No shared truth here. Coach (first few launches per repo), then go quiet.
114
+ const n = bumpNudge(cwd);
115
+ if (n <= 3) {
116
+ return `${C.sage('😮‍💨🤫 phewsh')} ${C.slate('· no shared project truth here yet')}\n`
117
+ + ` ${C.slate('so every AI tool stays in sync, create it:')} ${C.cream('phewsh intent --init')} ${C.slate('· → ' + bin)}`;
118
+ }
119
+ return `${C.sage('😮‍💨🤫 phewsh active')} ${C.slate('· ' + bin)}`;
83
120
  }
84
- } catch { /* never break the banner */ }
85
- return `😮‍💨🤫 phewsh active · ${intent}${record} · → ${bin}`;
121
+ // Truth present show the value + continuity health.
122
+ const files = fs.readdirSync(intentDir).filter(f => /\.(md|json)$/.test(f)).length;
123
+ const vision = firstLine(path.join(intentDir, 'vision.md'));
124
+ const next = firstLine(path.join(intentDir, 'next.md')) || firstLine(path.join(intentDir, 'status.md'));
125
+ const decisions = decisionCount(cwd);
126
+ let health = '✓ intent';
127
+ try {
128
+ const { statusDrift } = require('./truth');
129
+ const d = statusDrift(cwd);
130
+ if (d && d.tracked) health += d.commitsSince > 0 ? ` ${C.peach('⚠ record ' + d.commitsSince + ' behind')}` : ' ✓ record current';
131
+ } catch { /* nicety */ }
132
+
133
+ const lines = [`${C.sage('😮‍💨🤫 phewsh active')} ${C.slate(health)} ${C.slate('· → ' + bin)}`];
134
+ if (vision) lines.push(` ${C.slate('vision:')} ${C.cream(vision)}`);
135
+ if (next) lines.push(` ${C.slate('next: ')} ${C.cream(next)}${decisions ? C.slate(' · ' + decisions + ' decisions') : ''}`);
136
+ else if (decisions) lines.push(` ${C.slate(decisions + ' decisions · ' + files + ' intent files')}`);
137
+ return lines.join('\n');
138
+ } catch {
139
+ return `😮‍💨🤫 phewsh active · → ${bin}`; // never break the launch
140
+ }
86
141
  }
87
142
 
88
143
  // ── shell rc (PATH activation) ──────────────────────────────────────────
@@ -0,0 +1,98 @@
1
+ // phewsh slash commands — teach ONE verb, not a tool.
2
+ //
3
+ // CG's insight: the first thing a user should learn isn't `phewsh`, it's
4
+ // `/intent`. If every AI tool has `/intent`, the user learns the BEHAVIOR they
5
+ // want (see this project's truth) without learning the implementation. So
6
+ // phewsh installs `/intent` into each harness's NATIVE custom-command slot —
7
+ // a structured, user-invoked channel (not injected prose). It only does
8
+ // something when the user types it, so there's nothing for a model to flag.
9
+ //
10
+ // Each harness has its own format/dir; we only install for tools present, and
11
+ // never clobber a command the user wrote (we mark ours + check the marker).
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+
17
+ const MARKER = 'phewsh-managed';
18
+
19
+ // The portable prompt `/intent` runs. It's a user-invoked request to surface
20
+ // the project's own .intent/ docs — trusted because the user asked for it.
21
+ const INTENT_PROMPT = `Show me this project's current intent, read from its \`.intent/\` directory. Be concise:
22
+ - **Vision** — what we're building and why
23
+ - **Current objective / next** — what's in flight
24
+ - **Constraints** — budget, time, scope, non-negotiables
25
+ - **Recent decisions** — and any that are still open
26
+
27
+ If there is no \`.intent/\` directory here, say so and tell me I can create one with \`phewsh intent --init\` so every AI tool I use shares the same project truth.`;
28
+
29
+ // Tools that support file-based custom slash commands, and how each wants them.
30
+ // parentDir must already exist (the tool is installed) before we write.
31
+ //
32
+ // Claude Code is deliberately EXCLUDED: it already ships a phewsh `/intent`
33
+ // SKILL (the artifact generator) and gets the SessionStart hook brief, so a
34
+ // global `/intent` command here just collides with the canonical one (two
35
+ // `/intent` entries). We add `/intent` only where it's genuinely absent.
36
+ const SLASH_TARGETS = [
37
+ { id: 'codex', parent: '.codex', sub: 'prompts', file: 'intent.md', fmt: 'codex' },
38
+ { id: 'gemini', parent: '.gemini', sub: 'commands', file: 'intent.toml', fmt: 'gemini' },
39
+ ];
40
+
41
+ function bodyFor(fmt) {
42
+ if (fmt === 'claude') {
43
+ return `---\ndescription: Show this project's intent — vision, objective, constraints, decisions (phewsh)\n---\n<!-- ${MARKER} · remove with: phewsh ambient off -->\n\n${INTENT_PROMPT}\n`;
44
+ }
45
+ if (fmt === 'codex') {
46
+ return `<!-- ${MARKER} · remove with: phewsh ambient off -->\n${INTENT_PROMPT}\n`;
47
+ }
48
+ if (fmt === 'gemini') {
49
+ const esc = INTENT_PROMPT.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
50
+ return `# ${MARKER} · remove with: phewsh ambient off\ndescription = "Show this project's intent via phewsh"\nprompt = """\n${esc}\n"""\n`;
51
+ }
52
+ return INTENT_PROMPT;
53
+ }
54
+
55
+ function targetPath(t) {
56
+ return path.join(os.homedir(), t.parent, t.sub, t.file);
57
+ }
58
+
59
+ // Install /intent for every present tool. Won't overwrite a user's own command
60
+ // (only a file we previously marked, or a fresh write). Returns written ids.
61
+ function installSlashCommands() {
62
+ const written = [];
63
+ for (const t of SLASH_TARGETS) {
64
+ try {
65
+ if (!fs.existsSync(path.join(os.homedir(), t.parent))) continue; // tool not installed
66
+ const fp = targetPath(t);
67
+ if (fs.existsSync(fp)) {
68
+ const cur = fs.readFileSync(fp, 'utf-8');
69
+ if (!cur.includes(MARKER)) continue; // user's own /intent — leave it
70
+ if (cur === bodyFor(t.fmt)) continue; // already current
71
+ }
72
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
73
+ fs.writeFileSync(fp, bodyFor(t.fmt));
74
+ written.push(t.id);
75
+ } catch { /* best-effort per tool */ }
76
+ }
77
+ return { written };
78
+ }
79
+
80
+ function removeSlashCommands() {
81
+ const removed = [];
82
+ for (const t of SLASH_TARGETS) {
83
+ try {
84
+ const fp = targetPath(t);
85
+ if (fs.existsSync(fp) && fs.readFileSync(fp, 'utf-8').includes(MARKER)) {
86
+ fs.unlinkSync(fp);
87
+ removed.push(t.id);
88
+ }
89
+ } catch { /* best-effort */ }
90
+ }
91
+ return { removed };
92
+ }
93
+
94
+ function detectSlashTargets() {
95
+ return SLASH_TARGETS.filter(t => fs.existsSync(path.join(os.homedir(), t.parent)));
96
+ }
97
+
98
+ module.exports = { installSlashCommands, removeSlashCommands, detectSlashTargets, INTENT_PROMPT };
package/lib/suggest.js CHANGED
@@ -38,6 +38,7 @@ function suggestAll(s = {}) {
38
38
  turnsThisSession = 0,
39
39
  seqStale = false,
40
40
  ambientOn = false,
41
+ shimOn = false,
41
42
  commitsSinceIntent = 0,
42
43
  bestKeeper = null, // { route, label, keptRate, total } from the record, or null
43
44
  } = s;
@@ -132,6 +133,18 @@ function suggestAll(s = {}) {
132
133
  });
133
134
  }
134
135
 
136
+ // 6. Shim off — the guaranteed proof-of-life. phewsh prints a status banner
137
+ // when you launch any tool, then runs the real tool. Offer once it's not on.
138
+ if (!shimOn && installedHarnesses.length > 0) {
139
+ out.push({
140
+ id: 'enable-shim',
141
+ priority: 48,
142
+ message: `See phewsh work every time you launch a tool — a status banner (intent loaded, record current) before Claude/Codex/etc.`,
143
+ command: 'phewsh shim on',
144
+ why: 'phewsh prints the proof itself, so it\'s guaranteed; the real tool always runs. Reversible: `phewsh shim off`.',
145
+ });
146
+ }
147
+
135
148
  return out.sort((a, b) => b.priority - a.priority);
136
149
  }
137
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.42",
3
+ "version": "0.15.44",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"