@albinocrabs/feynman 0.2.2 → 0.2.5

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/feynman.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // bin/feynman.js — feynman unified CLI
3
- // Subcommands: install, uninstall, doctor, lint, version, help
3
+ // Subcommands: install, uninstall, doctor, lint, examples, bootstrap, version, help
4
4
  // Zero runtime deps. CJS only. Node >= 18.
5
5
  'use strict';
6
6
 
@@ -22,16 +22,22 @@ const c = {
22
22
 
23
23
  const PKG = require('../package.json');
24
24
  const VERSION = PKG.version;
25
+ const ROOT_DIR = path.resolve(__dirname, '..');
25
26
 
26
27
  // Resolve paths using os.homedir() — never tilde literal (bug #8810)
27
28
  const HOME = os.homedir();
28
29
 
29
30
  // Hook script lives relative to this file
30
31
  const HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-activate.js');
32
+ const SESSION_HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-session-start.js');
31
33
  const RULES_PATH = path.resolve(__dirname, '..', 'rules', 'feynman-activate.md');
32
34
 
33
35
  const DEFAULT_STATE = { enabled: true, intensity: 'full', injections: 0 };
34
- const VALID_TARGETS = ['claude', 'codex', 'both'];
36
+ const TARGET_ALIASES = {
37
+ all: 'both',
38
+ '*': 'both',
39
+ };
40
+ const VALID_TARGETS = ['claude', 'codex', 'both', 'all', '*'];
35
41
 
36
42
  function targetConfig(name) {
37
43
  const dirName = name === 'codex' ? '.codex' : '.claude';
@@ -52,7 +58,7 @@ function targetNames(target) {
52
58
  return target === 'both' ? ['claude', 'codex'] : [target];
53
59
  }
54
60
 
55
- function parseTarget(args, fallback = 'claude') {
61
+ function parseTarget(args, fallback = 'codex') {
56
62
  let target = fallback;
57
63
  const keep = [];
58
64
  for (let i = 0; i < args.length; i++) {
@@ -66,9 +72,10 @@ function parseTarget(args, fallback = 'claude') {
66
72
  }
67
73
  }
68
74
  if (!VALID_TARGETS.includes(target)) {
69
- console.error(`feynman: invalid --target '${target}' (expected claude, codex, or both)`);
75
+ console.error(`feynman: invalid --target '${target}' (expected claude, codex, both, all, or *)`);
70
76
  process.exit(2);
71
77
  }
78
+ target = TARGET_ALIASES[target] || target;
72
79
  return { target, args: keep };
73
80
  }
74
81
 
@@ -89,30 +96,329 @@ ${c.bold('Commands:')}
89
96
  uninstall Remove feynman hook (state preserved)
90
97
  doctor Check installation health
91
98
  lint <file> Lint a markdown file for diagram rule violations
99
+ examples List and display example prompts from the repository
100
+ bootstrap Export shared Feynman assets to a local folder
92
101
  version Print version number
93
102
  help Show this help
94
103
 
95
104
  ${c.bold('Options:')}
96
105
  --help, -h Show help for a command
97
- --target claude | codex | both (default: claude)
106
+ --target claude | codex | both | all | *
98
107
  --force (install) Re-register even if already installed
99
108
 
100
109
  ${c.bold('Examples:')}
101
110
  npx @albinocrabs/feynman install
102
111
  npx @albinocrabs/feynman install --target codex
103
112
  npx @albinocrabs/feynman install --target both
113
+ npx @albinocrabs/feynman install --target all
104
114
  npx @albinocrabs/feynman doctor
105
115
  feynman lint response.md
116
+ feynman bootstrap --out ./feynman-package
117
+ feynman examples
106
118
  feynman uninstall
107
119
  `;
108
120
 
121
+ const EXAMPLES_HELP = `${c.bold('feynman examples')} — print built-in demonstration prompts
122
+
123
+ ${c.bold('Usage:')}
124
+ feynman examples # list available examples
125
+ feynman examples --name <fileBase> # print a specific example
126
+ feynman examples --random # print a random example
127
+
128
+ ${c.bold('Options:')}
129
+ --name Example filename without .md extension (examples/feature-planning)
130
+ --random Show one random example in full
131
+ --help Show this help
132
+
133
+ Example filenames:
134
+ - architecture-review
135
+ - api-flow
136
+ - c4-platform-diagramming
137
+ - db-schema
138
+ - algorithm-explain
139
+ - deploy-pipeline
140
+ - code-review
141
+ - incident-response
142
+ - feature-planning
143
+ `;
144
+
145
+ const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples');
146
+ const SKILL_SRC = path.resolve(ROOT_DIR, 'skills', 'feynman', 'SKILL.md');
147
+ const CLAUDE_PLUGIN = path.resolve(ROOT_DIR, '.claude-plugin', 'plugin.json');
148
+ const CODEX_PLUGIN = path.resolve(ROOT_DIR, '.codex-plugin', 'plugin.json');
149
+ const PACKAGE_HOOKS = path.resolve(ROOT_DIR, 'hooks', 'hooks.json');
150
+ const DEFAULT_BOOTSTRAP_DIR = 'feynman-package';
151
+ const ACTIVATOR_JS = path.resolve(ROOT_DIR, 'hooks', 'feynman-activate.js');
152
+ const CLI_JS = path.resolve(ROOT_DIR, 'bin', 'feynman.js');
153
+ const PACKAGE_JSON = path.resolve(ROOT_DIR, 'package.json');
154
+
155
+ const BOOTSTRAP_HELP = `${c.bold('feynman bootstrap')} — export Feynman assets into local folder
156
+
157
+ ${c.bold('Usage:')}
158
+ feynman bootstrap
159
+ feynman bootstrap --out <directory>
160
+
161
+ ${c.bold('Options:')}
162
+ --out Output folder (default: ./feynman-package)
163
+ --force Recreate output folder if it exists
164
+ --help Show this help
165
+ `;
166
+
167
+ function ensureDir(dir) {
168
+ fs.mkdirSync(dir, { recursive: true });
169
+ }
170
+
171
+ function copyFileIfExists(src, dest) {
172
+ if (!fs.existsSync(src)) return false;
173
+ ensureDir(path.dirname(dest));
174
+ fs.copyFileSync(src, dest);
175
+ return true;
176
+ }
177
+
178
+ function copyMarkdownDir(src, dest) {
179
+ if (!fs.existsSync(src)) return 0;
180
+ let copied = 0;
181
+ for (const entry of fs.readdirSync(src, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
182
+ const sourcePath = path.join(src, entry.name);
183
+ const destPath = path.join(dest, entry.name);
184
+
185
+ if (entry.isDirectory()) {
186
+ copied += copyMarkdownDir(sourcePath, destPath);
187
+ continue;
188
+ }
189
+
190
+ if (!entry.isFile() || !entry.name.endsWith('.md')) {
191
+ continue;
192
+ }
193
+
194
+ ensureDir(path.dirname(destPath));
195
+ fs.copyFileSync(sourcePath, destPath);
196
+ copied += 1;
197
+ }
198
+ return copied;
199
+ }
200
+
201
+ function examplesIndex() {
202
+ if (!fs.existsSync(EXAMPLES_DIR)) return [];
203
+
204
+ return fs.readdirSync(EXAMPLES_DIR)
205
+ .filter((name) => name.endsWith('.md'))
206
+ .sort()
207
+ .map((name) => {
208
+ const file = path.join(EXAMPLES_DIR, name);
209
+ const content = fs.readFileSync(file, 'utf8');
210
+ const title = (content.match(/^#\s*(.+)$/m) || [null, name])[1].trim();
211
+ const question = (content.match(/^> (.*)$/m) || [null, ''])[1].trim();
212
+ return {
213
+ name: name.replace(/\.md$/, ''),
214
+ title,
215
+ question,
216
+ path: file,
217
+ };
218
+ });
219
+ }
220
+
221
+ function cmdExamples(args) {
222
+ if (args.includes('--help') || args.includes('-h')) {
223
+ console.log(EXAMPLES_HELP);
224
+ process.exit(0);
225
+ }
226
+
227
+ const entries = examplesIndex();
228
+ if (!entries.length) {
229
+ console.log('No examples found under examples/.');
230
+ process.exit(0);
231
+ }
232
+
233
+ let random = false;
234
+ let wantsName = null;
235
+ const unknown = [];
236
+
237
+ for (let i = 0; i < args.length; i += 1) {
238
+ const arg = args[i];
239
+ if (arg === '--name' || arg === '-n') {
240
+ const value = args[i + 1];
241
+ if (!value || value.startsWith('-')) {
242
+ console.error('feynman examples: --name requires a value');
243
+ process.exit(2);
244
+ }
245
+ if (wantsName !== null) {
246
+ console.error('feynman examples: duplicate --name');
247
+ process.exit(2);
248
+ }
249
+ wantsName = value;
250
+ i += 1;
251
+ continue;
252
+ }
253
+
254
+ if (arg === '--random' || arg === '-r') {
255
+ random = true;
256
+ continue;
257
+ }
258
+
259
+ if (arg.startsWith('-')) {
260
+ unknown.push(arg);
261
+ continue;
262
+ }
263
+
264
+ unknown.push(arg);
265
+ }
266
+
267
+ if (random && wantsName) {
268
+ console.error('feynman examples: use either --random or --name');
269
+ process.exit(2);
270
+ }
271
+
272
+ if (unknown.length > 0) {
273
+ console.error(`feynman examples: unexpected arguments "${unknown.join(' ')}"`);
274
+ console.error('Run `feynman examples --help` for usage.');
275
+ process.exit(2);
276
+ }
277
+
278
+ if (random) {
279
+ const entry = entries[Math.floor(Math.random() * entries.length)];
280
+ const content = fs.readFileSync(entry.path, 'utf8');
281
+ console.log(`\n[${entry.name}] ${entry.title}\n`);
282
+ console.log('Question:');
283
+ console.log(entry.question ? `> ${entry.question}` : '(no question marker found)');
284
+ console.log('\nPreview:\n');
285
+ const lines = content.split('\n').slice(0, 26);
286
+ console.log(lines.join('\n'));
287
+ process.exit(0);
288
+ }
289
+
290
+ if (wantsName) {
291
+ const entry = entries.find((item) => item.name === wantsName);
292
+ if (!entry) {
293
+ console.error(`feynman examples: unknown example '${wantsName}'`);
294
+ process.exit(2);
295
+ }
296
+ const content = fs.readFileSync(entry.path, 'utf8');
297
+ console.log(`\n[${entry.name}] ${entry.title}\n`);
298
+ console.log(content);
299
+ process.exit(0);
300
+ }
301
+
302
+ if (args.length === 0) {
303
+ console.log('Available examples:\n');
304
+ for (const entry of entries) {
305
+ const q = entry.question ? ` — ${entry.question}` : '';
306
+ console.log(`- ${entry.name}`);
307
+ console.log(` ${entry.title}${q ? ` — ${q}` : ''}`);
308
+ }
309
+ process.exit(0);
310
+ }
311
+ }
312
+
313
+ function cmdBootstrap(args) {
314
+ if (args.includes('--help') || args.includes('-h')) {
315
+ console.log(BOOTSTRAP_HELP);
316
+ process.exit(0);
317
+ }
318
+
319
+ let out = path.resolve(process.cwd(), DEFAULT_BOOTSTRAP_DIR);
320
+ let force = false;
321
+ const unknown = [];
322
+
323
+ for (let i = 0; i < args.length; i += 1) {
324
+ const arg = args[i];
325
+
326
+ if (arg === '--force') {
327
+ force = true;
328
+ continue;
329
+ }
330
+
331
+ if (arg === '--out') {
332
+ const value = args[i + 1];
333
+ if (!value || value.startsWith('-')) {
334
+ console.error('feynman bootstrap: --out requires a value');
335
+ process.exit(2);
336
+ }
337
+ out = path.resolve(process.cwd(), value);
338
+ i += 1;
339
+ continue;
340
+ }
341
+
342
+ if (arg.startsWith('--out=')) {
343
+ const value = arg.slice('--out='.length);
344
+ if (!value) {
345
+ console.error('feynman bootstrap: invalid --out argument');
346
+ process.exit(2);
347
+ }
348
+ out = path.resolve(process.cwd(), value);
349
+ continue;
350
+ }
351
+
352
+ if (arg.startsWith('-')) {
353
+ unknown.push(arg);
354
+ continue;
355
+ }
356
+
357
+ unknown.push(arg);
358
+ }
359
+
360
+ if (unknown.length > 0) {
361
+ console.error(`feynman bootstrap: unexpected arguments "${unknown.join(' ')}"`);
362
+ console.error('Run `feynman bootstrap --help` for usage.');
363
+ process.exit(2);
364
+ }
365
+
366
+ if (fs.existsSync(out) && !force) {
367
+ console.log(`feynman bootstrap: output already exists at ${out}`);
368
+ console.log('Use `--force` to recreate it.');
369
+ process.exit(0);
370
+ }
371
+
372
+ if (fs.existsSync(out)) {
373
+ fs.rmSync(out, { recursive: true, force: true });
374
+ }
375
+
376
+ const counts = {
377
+ examples: copyMarkdownDir(EXAMPLES_DIR, path.join(out, 'examples')),
378
+ rules: copyFileIfExists(RULES_PATH, path.join(out, 'rules', 'feynman-activate.md')) ? 1 : 0,
379
+ hooks: copyFileIfExists(PACKAGE_HOOKS, path.join(out, 'hooks', 'hooks.json')) ? 1 : 0,
380
+ hookRuntime: copyFileIfExists(ACTIVATOR_JS, path.join(out, 'hooks', 'feynman-activate.js')) ? 1 : 0,
381
+ cliRuntime: copyFileIfExists(CLI_JS, path.join(out, 'bin', 'feynman.js')) ? 1 : 0,
382
+ packageManifest: copyFileIfExists(PACKAGE_JSON, path.join(out, 'package.json')) ? 1 : 0,
383
+ plugins:
384
+ (copyFileIfExists(CLAUDE_PLUGIN, path.join(out, '.claude-plugin', 'plugin.json')) ? 1 : 0) +
385
+ (copyFileIfExists(CODEX_PLUGIN, path.join(out, '.codex-plugin', 'plugin.json')) ? 1 : 0),
386
+ skill: copyFileIfExists(SKILL_SRC, path.join(out, 'skills', 'feynman', 'SKILL.md')) ? 1 : 0,
387
+ };
388
+
389
+ ensureDir(out);
390
+ fs.writeFileSync(
391
+ path.join(out, 'feynman-bootstrap.json'),
392
+ JSON.stringify({
393
+ version: VERSION,
394
+ createdAt: new Date().toISOString(),
395
+ outputDir: out,
396
+ counts,
397
+ }, null, 2) + '\n'
398
+ );
399
+
400
+ const total = Object.values(counts).reduce((sum, count) => sum + (count || 0), 1);
401
+ console.log('');
402
+ console.log('┌─ feynman bootstrap ────────────────────────────────────────┐');
403
+ console.log(`│ output: ${out}`);
404
+ console.log(`│ examples: ${counts.examples}`);
405
+ console.log(`│ rules: ${counts.rules}`);
406
+ console.log(`│ hooks: ${counts.hooks}`);
407
+ console.log(`│ runtime: ${counts.hookRuntime + counts.cliRuntime + counts.packageManifest}`);
408
+ console.log(`│ plugins: ${counts.plugins}`);
409
+ console.log(`│ skill: ${counts.skill}`);
410
+ console.log(`│ files: ${total}`);
411
+ console.log('└───────────────────────────────────────────────────────────┘');
412
+ process.exit(0);
413
+ }
414
+
109
415
  const INSTALL_HELP = `${c.bold('feynman install')} — register feynman hook
110
416
 
111
417
  ${c.bold('Usage:')}
112
- feynman install [--target claude|codex|both] [--force]
418
+ feynman install [--target claude|codex|both|all|*] [--force]
113
419
 
114
420
  ${c.bold('Options:')}
115
- --target Install into Claude Code, Codex, or both (default: claude)
421
+ --target Install into Claude Code, Codex, both, all, or * (default: codex)
116
422
  --force Re-register hook even if already installed
117
423
 
118
424
  Claude creates:
@@ -120,17 +426,17 @@ Claude creates:
120
426
  ~/.claude/.feynman-active — presence flag
121
427
 
122
428
  Codex creates:
123
- ~/.codex/hooks.json — UserPromptSubmit hook registration
429
+ ~/.codex/hooks.json — SessionStart + UserPromptSubmit hook registration
124
430
  ~/.codex/.feynman/state.json — feynman state (enabled, intensity, injections)
125
431
  ~/.codex/.feynman-active — presence flag
126
432
 
127
- Idempotent by default: skips if feynman-activate.js entry already exists.
433
+ Idempotent by default: skips if feynman hook entries already exist.
128
434
  `;
129
435
 
130
436
  const UNINSTALL_HELP = `${c.bold('feynman uninstall')} — remove feynman hook
131
437
 
132
438
  ${c.bold('Usage:')}
133
- feynman uninstall [--target claude|codex|both]
439
+ feynman uninstall [--target claude|codex|both|all|*]
134
440
 
135
441
  Removes feynman hook entries from target config.
136
442
  Preserves .feynman/state.json (user data).
@@ -142,15 +448,15 @@ Idempotent: safe to run multiple times.
142
448
  const DOCTOR_HELP = `${c.bold('feynman doctor')} — check feynman installation health
143
449
 
144
450
  ${c.bold('Usage:')}
145
- feynman doctor [--target claude|codex]
451
+ feynman doctor [--target claude|codex|both|all|*]
146
452
 
147
453
  Checks:
148
454
  1. target hook config present
149
- 2. UserPromptSubmit hook references feynman-activate.js
150
- 3. Hook script file exists and is readable
455
+ 2. SessionStart and UserPromptSubmit hooks reference feynman scripts
456
+ 3. Hook script files exist and are readable
151
457
  4. Rules file exists and is non-empty
152
458
  5. state.json valid JSON with enabled field
153
- 6. .feynman-active flag present
459
+ 6. .feynman-active flag matches enabled state
154
460
  7. (INFO) lint hook registered (optional)
155
461
 
156
462
  Exit code: always 0 (advisory only).
@@ -202,23 +508,31 @@ function writeSettings(target, settings) {
202
508
  }
203
509
 
204
510
  function hasFeynmanHook(settings) {
205
- return ((settings.hooks && settings.hooks.UserPromptSubmit) || []).some(g =>
511
+ const promptHook = ((settings.hooks && settings.hooks.UserPromptSubmit) || []).some(g =>
206
512
  g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
207
513
  );
514
+ const sessionHook = ((settings.hooks && settings.hooks.SessionStart) || []).some(g =>
515
+ g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-session-start.js'))
516
+ );
517
+ return promptHook && sessionHook;
208
518
  }
209
519
 
210
520
  function removeFeynmanHooks(settings) {
211
- if (!settings.hooks || !Array.isArray(settings.hooks.UserPromptSubmit)) return settings;
212
- settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(g =>
213
- !(g.hooks && g.hooks.some(h =>
214
- h.command && (
215
- h.command.includes('feynman-activate.js') ||
216
- h.command.includes('feynman-lint.js')
217
- )
218
- ))
219
- );
220
- if (settings.hooks.UserPromptSubmit.length === 0) {
221
- delete settings.hooks.UserPromptSubmit;
521
+ if (!settings.hooks) return settings;
522
+ for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
523
+ if (!Array.isArray(settings.hooks[eventName])) continue;
524
+ settings.hooks[eventName] = settings.hooks[eventName].filter(g =>
525
+ !(g.hooks && g.hooks.some(h =>
526
+ h.command && (
527
+ h.command.includes('feynman-session-start.js') ||
528
+ h.command.includes('feynman-activate.js') ||
529
+ h.command.includes('feynman-lint.js')
530
+ )
531
+ ))
532
+ );
533
+ if (settings.hooks[eventName].length === 0) {
534
+ delete settings.hooks[eventName];
535
+ }
222
536
  }
223
537
  if (Object.keys(settings.hooks).length === 0) {
224
538
  delete settings.hooks;
@@ -229,10 +543,21 @@ function removeFeynmanHooks(settings) {
229
543
  function bootstrapState(target) {
230
544
  const cfg = targetConfig(target);
231
545
  fs.mkdirSync(cfg.feynmanDir, { recursive: true });
546
+ let state = DEFAULT_STATE;
232
547
  if (!fs.existsSync(cfg.statePath)) {
233
548
  fs.writeFileSync(cfg.statePath, JSON.stringify(DEFAULT_STATE, null, 2) + '\n');
549
+ } else {
550
+ try {
551
+ state = { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(cfg.statePath, 'utf8')) };
552
+ } catch (_) {
553
+ fs.writeFileSync(cfg.statePath, JSON.stringify(DEFAULT_STATE, null, 2) + '\n');
554
+ }
555
+ }
556
+ if (state.enabled) {
557
+ fs.writeFileSync(cfg.flagPath, state.intensity || DEFAULT_STATE.intensity);
558
+ } else if (fs.existsSync(cfg.flagPath)) {
559
+ fs.unlinkSync(cfg.flagPath);
234
560
  }
235
- fs.writeFileSync(cfg.flagPath, DEFAULT_STATE.intensity);
236
561
  }
237
562
 
238
563
  function installClaudeCommand() {
@@ -256,6 +581,7 @@ function installOne(target, opts) {
256
581
  // Read or create settings
257
582
  const cfg = readSettings(target);
258
583
  cfg.hooks = cfg.hooks || {};
584
+ cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
259
585
  cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
260
586
 
261
587
  // Idempotency check
@@ -267,20 +593,36 @@ function installOne(target, opts) {
267
593
  return { target, already: true };
268
594
  }
269
595
 
270
- // If force + already, remove old entry first
596
+ // If force or partial legacy install, remove old feynman entries first.
271
597
  if (already && force) {
272
598
  removeFeynmanHooks(cfg);
273
599
  cfg.hooks = cfg.hooks || {};
600
+ cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
601
+ cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
602
+ } else if (!already) {
603
+ removeFeynmanHooks(cfg);
604
+ cfg.hooks = cfg.hooks || {};
605
+ cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
274
606
  cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
275
607
  }
276
608
 
277
- // Append hook entry
609
+ // Append hook entries
610
+ const sessionEntry = {
611
+ hooks: [{
612
+ type: 'command',
613
+ command: hookCommandFor(target).replace(HOOK_PATH, SESSION_HOOK_PATH),
614
+ timeout: 5,
615
+ }]
616
+ };
617
+ if (target === 'codex') {
618
+ sessionEntry.matcher = 'startup|resume';
619
+ }
620
+ cfg.hooks.SessionStart.push(sessionEntry);
278
621
  cfg.hooks.UserPromptSubmit.push({
279
622
  hooks: [{
280
623
  type: 'command',
281
624
  command: hookCommandFor(target),
282
625
  timeout: 5,
283
- statusMessage: 'Injecting diagram rules...',
284
626
  }]
285
627
  });
286
628
 
@@ -318,7 +660,7 @@ function cmdInstall(opts) {
318
660
  }
319
661
  console.log('└──────────────────────────────────────────────────────────────┘');
320
662
  console.log('');
321
- console.log('Restart Claude Code or Codex to activate feynman.');
663
+ console.log('Restart Claude Code or Codex to activate feynman full mode.');
322
664
 
323
665
  process.exit(0);
324
666
  }
@@ -379,11 +721,24 @@ function cmdDoctor(opts = {}) {
379
721
  const settingsExists = fs.existsSync(tc.settingsPath);
380
722
  check(`${tc.settingsPath.replace(HOME, '~')} present`, settingsExists);
381
723
 
382
- // 2. UserPromptSubmit hook references feynman-activate.js
724
+ // 2. SessionStart and UserPromptSubmit hooks reference feynman scripts
725
+ let sessionHookRegistered = false;
383
726
  let hookRegistered = false;
727
+ let sessionHookAbsPath = null;
384
728
  let hookAbsPath = null;
385
729
  if (settingsExists) {
386
730
  const cfg = readSettings(target);
731
+ const sessionEntries = (cfg.hooks && cfg.hooks.SessionStart) || [];
732
+ const feynmanSessionEntry = sessionEntries.find(g =>
733
+ g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-session-start.js'))
734
+ );
735
+ sessionHookRegistered = !!feynmanSessionEntry;
736
+ if (feynmanSessionEntry) {
737
+ const hookCmd = feynmanSessionEntry.hooks.find(h => h.command && h.command.includes('feynman-session-start.js')).command;
738
+ const match = hookCmd.match(/"([^"]+feynman-session-start\.js)"/);
739
+ if (match) sessionHookAbsPath = match[1];
740
+ }
741
+
387
742
  const entries = (cfg.hooks && cfg.hooks.UserPromptSubmit) || [];
388
743
  const feynmanEntry = entries.find(g =>
389
744
  g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
@@ -396,9 +751,24 @@ function cmdDoctor(opts = {}) {
396
751
  if (match) hookAbsPath = match[1];
397
752
  }
398
753
  }
754
+ check('hook registered (feynman-session-start.js in SessionStart)', sessionHookRegistered);
399
755
  check('hook registered (feynman-activate.js in UserPromptSubmit)', hookRegistered);
400
756
 
401
- // 3. Hook script file exists + readable
757
+ // 3. Hook script files exist + readable
758
+ let sessionHookFileOk = false;
759
+ if (sessionHookAbsPath) {
760
+ try {
761
+ fs.accessSync(sessionHookAbsPath, fs.constants.R_OK);
762
+ sessionHookFileOk = true;
763
+ } catch (_) {}
764
+ } else if (sessionHookRegistered) {
765
+ try {
766
+ fs.accessSync(SESSION_HOOK_PATH, fs.constants.R_OK);
767
+ sessionHookFileOk = true;
768
+ } catch (_) {}
769
+ }
770
+ check('session hook script file exists and is readable', sessionHookFileOk);
771
+
402
772
  let hookFileOk = false;
403
773
  if (hookAbsPath) {
404
774
  try {
@@ -412,7 +782,7 @@ function cmdDoctor(opts = {}) {
412
782
  hookFileOk = true;
413
783
  } catch (_) {}
414
784
  }
415
- check('hook script file exists and is readable', hookFileOk);
785
+ check('prompt hook script file exists and is readable', hookFileOk);
416
786
 
417
787
  // 4. Rules file exists + non-empty
418
788
  let rulesOk = false;
@@ -424,15 +794,20 @@ function cmdDoctor(opts = {}) {
424
794
 
425
795
  // 5. state.json valid JSON + has enabled field
426
796
  let stateOk = false;
797
+ let stateEnabled = false;
427
798
  try {
428
799
  const state = JSON.parse(fs.readFileSync(tc.statePath, 'utf8'));
429
800
  stateOk = 'enabled' in state;
801
+ stateEnabled = state.enabled === true;
430
802
  } catch (_) {}
431
803
  check('state.json valid (has enabled field)', stateOk);
432
804
 
433
- // 6. .feynman-active flag present
805
+ // 6. .feynman-active flag matches state
434
806
  const flagPresent = fs.existsSync(tc.flagPath);
435
- check('.feynman-active flag present', flagPresent);
807
+ check(
808
+ stateEnabled ? '.feynman-active flag present when enabled' : '.feynman-active flag absent when disabled',
809
+ stateEnabled ? flagPresent : !flagPresent
810
+ );
436
811
 
437
812
  // 7. (INFO) lint hook registered
438
813
  let lintHookRegistered = false;
@@ -547,6 +922,14 @@ switch (sub) {
547
922
  cmdLint(rest);
548
923
  break;
549
924
  }
925
+ case 'examples': {
926
+ cmdExamples(rest);
927
+ break;
928
+ }
929
+ case 'bootstrap': {
930
+ cmdBootstrap(rest);
931
+ break;
932
+ }
550
933
  case 'version': {
551
934
  cmdVersion(rest);
552
935
  break;