@albinocrabs/feynman 0.2.4 → 0.2.6

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,6 +1,6 @@
1
1
  {
2
2
  "name": "feynman",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Auto-inject ASCII diagram rules into Codex and Claude Code prompts.",
5
5
  "author": {
6
6
  "name": "apolenkov",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## 0.2.6 - 2026-05-08
6
+
7
+ Changes since v0.2.5.
8
+
9
+ ### Features
10
+
11
+ - harden feynman runtime checks
12
+ - add roadmap phase uat risk patterns
13
+ - add sdlc output patterns to feynman
14
+ - add terminal-safe feynman rendering rules
15
+
16
+ ### Fixes
17
+
18
+ - clarify terminal table rendering rules
19
+ - harden feynman hook lifecycle
20
+
21
+ ### Documentation
22
+
23
+ - restore GSD validation coverage
24
+
25
+ ### Tests
26
+
27
+ - cover codex app-server hook visibility
28
+ - cover ci threshold branches
29
+
30
+ ### Maintenance
31
+
32
+ - use global gsd defaults
33
+ - remove gsd model bindings from repo config
34
+
35
+ ## 0.2.5 - 2026-05-08
36
+
37
+ ### Features
38
+
39
+ - add SessionStart hook priming alongside UserPromptSubmit reinforcement
40
+ - keep full diagram rules enabled by default for Claude Code and Codex
41
+
42
+ ### Fixes
43
+
44
+ - preserve explicit /feynman off behavior with silent hooks
45
+ - keep feynman hooks quiet by omitting status messages
46
+
47
+ ### Maintenance
48
+
49
+ - bump package and plugin manifest versions to 0.2.5
50
+ - expand tests for SessionStart registration and runtime behavior
51
+
5
52
  ## 0.2.4 - 2026-05-07
6
53
 
7
54
  Changes since v0.2.2.
package/README.md CHANGED
@@ -22,7 +22,10 @@
22
22
  <a href="docs/object-passport.md">Passport</a> •
23
23
  <a href="#before--after">Before/After</a> •
24
24
  <a href="#install">Install</a> •
25
+ <a href="#verify-the-install">Verify</a> •
25
26
  <a href="#intensity-levels">Levels</a> •
27
+ <a href="#security-notes">Security</a> •
28
+ <a href="#token-budget-and-output-size">Tokens</a> •
26
29
  <a href="#lint">Lint</a> •
27
30
  <a href="#examples">Examples</a> •
28
31
  <a href="docs/launch.md">Launch</a> •
@@ -32,8 +35,8 @@
32
35
  ---
33
36
 
34
37
  A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and Codex
35
- plugin that automatically injects ASCII diagram rules into every prompt via the
36
- `UserPromptSubmit` hook.
38
+ plugin that automatically injects ASCII diagram rules through `SessionStart`
39
+ and `UserPromptSubmit` hooks.
37
40
 
38
41
  ```bash
39
42
  npx -y @albinocrabs/feynman@latest install --target all
@@ -173,16 +176,15 @@ feynman hook instead of adding duplicates.
173
176
  git clone https://github.com/apolenkov/feynman && bash feynman/install.sh
174
177
  ```
175
178
 
176
- Restart Claude Code or Codex. Done.
177
-
178
- **Verify:** `npx @albinocrabs/feynman doctor --target claude` or `npx @albinocrabs/feynman doctor --target codex`
179
+ Restart Claude Code or Codex after install so a fresh `SessionStart` hook can
180
+ prime the session.
179
181
 
180
182
  **Uninstall:** `npx @albinocrabs/feynman uninstall --target claude|codex|both|all|*`
181
183
 
182
184
  **Plugin manifests:** this repo also ships `.claude-plugin/plugin.json`,
183
- `hooks/hooks.json`, `.codex-plugin/plugin.json`, and `hooks.json` so plugin
184
- marketplaces can discover feynman. The npx installer remains the production
185
- fallback because both clients still support direct user hook registration.
185
+ `hooks/hooks.json`, `.codex-plugin/plugin.json`, and `hooks.json` for direct
186
+ client integrations. The npx installer remains the production fallback because
187
+ both clients still support direct user hook registration.
186
188
 
187
189
  <details>
188
190
  <summary>Manual install</summary>
@@ -193,14 +195,25 @@ Add to `~/.claude/settings.json` — use the absolute path, not `~/`
193
195
  ```json
194
196
  {
195
197
  "hooks": {
198
+ "SessionStart": [
199
+ {
200
+ "matcher": "startup|resume",
201
+ "hooks": [
202
+ {
203
+ "type": "command",
204
+ "command": "node \"/absolute/path/to/feynman/hooks/feynman-session-start.js\"",
205
+ "timeout": 5
206
+ }
207
+ ]
208
+ }
209
+ ],
196
210
  "UserPromptSubmit": [
197
211
  {
198
212
  "hooks": [
199
213
  {
200
214
  "type": "command",
201
215
  "command": "node \"/absolute/path/to/feynman/hooks/feynman-activate.js\"",
202
- "timeout": 5,
203
- "statusMessage": "Injecting diagram rules..."
216
+ "timeout": 5
204
217
  }
205
218
  ]
206
219
  }
@@ -215,14 +228,25 @@ For Codex, add the same shape to `~/.codex/hooks.json` and set
215
228
  ```json
216
229
  {
217
230
  "hooks": {
231
+ "SessionStart": [
232
+ {
233
+ "matcher": "startup|resume",
234
+ "hooks": [
235
+ {
236
+ "type": "command",
237
+ "command": "FEYNMAN_HOME=\"$HOME/.codex\" node \"/absolute/path/to/feynman/hooks/feynman-session-start.js\"",
238
+ "timeout": 5
239
+ }
240
+ ]
241
+ }
242
+ ],
218
243
  "UserPromptSubmit": [
219
244
  {
220
245
  "hooks": [
221
246
  {
222
247
  "type": "command",
223
248
  "command": "FEYNMAN_HOME=\"$HOME/.codex\" node \"/absolute/path/to/feynman/hooks/feynman-activate.js\"",
224
- "timeout": 5,
225
- "statusMessage": "Injecting diagram rules..."
249
+ "timeout": 5
226
250
  }
227
251
  ]
228
252
  }
@@ -232,6 +256,41 @@ For Codex, add the same shape to `~/.codex/hooks.json` and set
232
256
  ```
233
257
  </details>
234
258
 
259
+ After install, feynman starts in `full` mode by default. Disable or change it
260
+ explicitly with `/feynman off`, `/feynman lite`, `/feynman full`, or
261
+ `/feynman ultra`.
262
+
263
+ ## Verify the install
264
+
265
+ Run `doctor` after installing or after manually editing hooks:
266
+
267
+ ```bash
268
+ npx @albinocrabs/feynman doctor --target codex
269
+ npx @albinocrabs/feynman doctor --target claude
270
+ npx @albinocrabs/feynman doctor --target all
271
+ ```
272
+
273
+ A healthy target reports both hook events and both hook script files as `OK`:
274
+
275
+ ```text
276
+ [OK] hook registered (feynman-session-start.js in SessionStart)
277
+ [OK] hook registered (feynman-activate.js in UserPromptSubmit)
278
+ [OK] session hook script file exists and is readable
279
+ [OK] prompt hook script file exists and is readable
280
+ Status: OK
281
+ ```
282
+
283
+ For Codex, runtime state should live under `~/.codex`:
284
+
285
+ ```bash
286
+ cat ~/.codex/.feynman/state.json
287
+ test -f ~/.codex/.feynman-active
288
+ ```
289
+
290
+ For Claude Code, use `~/.claude` instead of `~/.codex`. `doctor` fails if a
291
+ registered hook command cannot be parsed to a real hook script, which catches
292
+ stale or broken manual installs.
293
+
235
294
  ## Intensity Levels
236
295
 
237
296
  | Level | What draws | Use when |
@@ -276,6 +335,26 @@ This bypasses `~/.codex/hooks.json` hook execution entirely.
276
335
  Regular `/feynman off` and `/feynman on` continue to use normal profile state
277
336
  files (`~/.codex/.feynman-active`, `~/.codex/.feynman/state.json`).
278
337
 
338
+ ## Security notes
339
+
340
+ feynman hooks are local prompt-context hooks. They do not require network
341
+ access, do not read repository files, and do not read credentials. The active
342
+ mode is stored only in the client-local state path:
343
+
344
+ ```text
345
+ ~/.claude/.feynman/state.json
346
+ ~/.codex/.feynman/state.json
347
+ ```
348
+
349
+ The hook runtime treats invalid state as disabled for that turn, removes the
350
+ activation flag, and stays silent. That prevents a corrupted state file from
351
+ silently forcing diagram rules into future prompts.
352
+
353
+ `uninstall` removes only feynman hook commands and preserves unrelated hooks in
354
+ the same hook group. `doctor` validates that registered commands point to real
355
+ hook scripts, so broken manual commands are visible before a session depends on
356
+ them.
357
+
279
358
  ## CLI examples
280
359
 
281
360
  Quickly discover and view repository prompt templates:
@@ -481,24 +560,51 @@ from day one.
481
560
 
482
561
  ## How it works
483
562
 
484
- The `UserPromptSubmit` hook fires on every Claude Code or Codex prompt. The
485
- hook reads the target-local state file (`~/.claude/.feynman/state.json` or
486
- `~/.codex/.feynman/state.json`), extracts the rules for the active intensity
487
- level, and injects them as `additionalContext` — invisible to you, visible to
488
- the model on every message.
563
+ The `SessionStart` hook primes fresh Claude Code or Codex sessions with the
564
+ active rules, and the `UserPromptSubmit` hook reinforces them on every prompt.
565
+ Both hooks read the target-local state file
566
+ (`~/.claude/.feynman/state.json` or `~/.codex/.feynman/state.json`), extract
567
+ the rules for the active intensity level, and inject them into model context.
489
568
 
490
569
  ```
491
570
  [your prompt]
492
571
  +
493
- [feynman rules] injected by hook, ~2KB
572
+ [feynman rules] injected by hook
494
573
 
495
574
 
496
- [Claude]
575
+ [Claude Code]
576
+ or
577
+ [Codex]
497
578
 
498
579
 
499
580
  [structured response with ASCII diagrams]
500
581
  ```
501
582
 
583
+ ## Token budget and output size
584
+
585
+ feynman always adds some input context when it is enabled, because the active
586
+ diagram rules are injected into the client prompt context. It does not add
587
+ visible output by itself; output changes only when the assistant uses the
588
+ rules.
589
+
590
+ Current rule payload sizes are approximately:
591
+
592
+ | Mode | Bytes | Approx tokens | Use when |
593
+ | ---- | ----- | ------------- | -------- |
594
+ | `lite` | 1307 | 317 | minimal flows and trees |
595
+ | `full` | 2180 | 532 | default diagram coverage |
596
+ | `ultra` | 1390 | 337 | force diagrams more often |
597
+
598
+ The token count is a rough `chars / 4` estimate, not a billing counter. The
599
+ actual number depends on the runtime tokenizer and surrounding hook envelope.
600
+
601
+ The plugin can reduce output size when a diagram replaces repeated prose,
602
+ especially for flows, status summaries, hierarchies, and comparisons. It can
603
+ increase output for short answers where a diagram would be unnecessary, so the
604
+ rules explicitly suppress diagrams for one- or two-sentence direct answers.
605
+ Use `/feynman lite` for lower overhead or `/feynman off` when token budget is
606
+ more important than visual structure.
607
+
502
608
  ## Release process
503
609
 
504
610
  Every push runs tests on Node 18 and 20 across Ubuntu and macOS. The release
package/SECURITY.md CHANGED
@@ -39,4 +39,4 @@ Before publishing a new npm version:
39
39
  - GitHub release tag must match `package.json` version with a `v` prefix.
40
40
  - GitHub Actions secret `NPM_TOKEN` must be present for first publish of a new version.
41
41
  - npm provenance is enabled in the release workflow.
42
- - Registry smoke verification must pass after publish (`npm view`, install from npm, `feynman doctor --target both`).
42
+ - Registry smoke verification must pass after publish (`npm view`, install from npm, `feynman doctor --target all`).
package/bin/feynman.js CHANGED
@@ -29,6 +29,7 @@ const HOME = os.homedir();
29
29
 
30
30
  // Hook script lives relative to this file
31
31
  const HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-activate.js');
32
+ const SESSION_HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-session-start.js');
32
33
  const RULES_PATH = path.resolve(__dirname, '..', 'rules', 'feynman-activate.md');
33
34
 
34
35
  const DEFAULT_STATE = { enabled: true, intensity: 'full', injections: 0 };
@@ -108,7 +109,7 @@ ${c.bold('Options:')}
108
109
  ${c.bold('Examples:')}
109
110
  npx @albinocrabs/feynman install
110
111
  npx @albinocrabs/feynman install --target codex
111
- npx @albinocrabs/feynman install --target both
112
+ npx @albinocrabs/feynman install --target all
112
113
  npx @albinocrabs/feynman install --target all
113
114
  npx @albinocrabs/feynman doctor
114
115
  feynman lint response.md
@@ -425,11 +426,11 @@ Claude creates:
425
426
  ~/.claude/.feynman-active — presence flag
426
427
 
427
428
  Codex creates:
428
- ~/.codex/hooks.json — UserPromptSubmit hook registration
429
+ ~/.codex/hooks.json — SessionStart + UserPromptSubmit hook registration
429
430
  ~/.codex/.feynman/state.json — feynman state (enabled, intensity, injections)
430
431
  ~/.codex/.feynman-active — presence flag
431
432
 
432
- Idempotent by default: skips if feynman-activate.js entry already exists.
433
+ Idempotent by default: skips if feynman hook entries already exist.
433
434
  `;
434
435
 
435
436
  const UNINSTALL_HELP = `${c.bold('feynman uninstall')} — remove feynman hook
@@ -451,11 +452,11 @@ ${c.bold('Usage:')}
451
452
 
452
453
  Checks:
453
454
  1. target hook config present
454
- 2. UserPromptSubmit hook references feynman-activate.js
455
- 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
456
457
  4. Rules file exists and is non-empty
457
458
  5. state.json valid JSON with enabled field
458
- 6. .feynman-active flag present
459
+ 6. .feynman-active flag matches enabled state
459
460
  7. (INFO) lint hook registered (optional)
460
461
 
461
462
  Exit code: always 0 (advisory only).
@@ -506,24 +507,61 @@ function writeSettings(target, settings) {
506
507
  fs.writeFileSync(cfg.settingsPath, JSON.stringify(settings, null, 2) + '\n');
507
508
  }
508
509
 
510
+ function isFeynmanHookCommand(command) {
511
+ return typeof command === 'string' && (
512
+ command.includes('feynman-session-start.js') ||
513
+ command.includes('feynman-activate.js') ||
514
+ command.includes('feynman-lint.js')
515
+ );
516
+ }
517
+
509
518
  function hasFeynmanHook(settings) {
510
- return ((settings.hooks && settings.hooks.UserPromptSubmit) || []).some(g =>
519
+ const promptHook = ((settings.hooks && settings.hooks.UserPromptSubmit) || []).some(g =>
511
520
  g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
512
521
  );
522
+ const sessionHook = ((settings.hooks && settings.hooks.SessionStart) || []).some(g =>
523
+ g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-session-start.js'))
524
+ );
525
+ return promptHook && sessionHook;
513
526
  }
514
527
 
515
- function removeFeynmanHooks(settings) {
516
- if (!settings.hooks || !Array.isArray(settings.hooks.UserPromptSubmit)) return settings;
517
- settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(g =>
518
- !(g.hooks && g.hooks.some(h =>
519
- h.command && (
520
- h.command.includes('feynman-activate.js') ||
521
- h.command.includes('feynman-lint.js')
522
- )
523
- ))
528
+ function hasAnyFeynmanHook(settings) {
529
+ if (!settings.hooks) return false;
530
+ return ['SessionStart', 'UserPromptSubmit', 'Stop'].some(eventName =>
531
+ ((settings.hooks && settings.hooks[eventName]) || []).some(g =>
532
+ g.hooks && g.hooks.some(h => isFeynmanHookCommand(h.command))
533
+ )
524
534
  );
525
- if (settings.hooks.UserPromptSubmit.length === 0) {
526
- delete settings.hooks.UserPromptSubmit;
535
+ }
536
+
537
+ function extractHookScriptPath(command, scriptName) {
538
+ if (typeof command !== 'string') return null;
539
+ const escaped = scriptName.replace(/\./g, '\\.');
540
+ const quotedPattern = new RegExp("[\"']([^\"']*" + escaped + ")[\"']");
541
+ const quoted = command.match(quotedPattern);
542
+ if (quoted) return quoted[1];
543
+
544
+ const unquotedPattern = new RegExp("(?:^|\\s)(/[^\\s\"';&|<>]*" + escaped + ")(?=$|\\s)");
545
+ const unquoted = command.match(unquotedPattern);
546
+ return unquoted ? unquoted[1] : null;
547
+ }
548
+
549
+ function removeFeynmanHooks(settings) {
550
+ if (!settings.hooks) return settings;
551
+ for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
552
+ if (!Array.isArray(settings.hooks[eventName])) continue;
553
+ settings.hooks[eventName] = settings.hooks[eventName]
554
+ .map(g => {
555
+ if (!Array.isArray(g.hooks)) return g;
556
+ return {
557
+ ...g,
558
+ hooks: g.hooks.filter(h => !isFeynmanHookCommand(h.command)),
559
+ };
560
+ })
561
+ .filter(g => !Array.isArray(g.hooks) || g.hooks.length > 0);
562
+ if (settings.hooks[eventName].length === 0) {
563
+ delete settings.hooks[eventName];
564
+ }
527
565
  }
528
566
  if (Object.keys(settings.hooks).length === 0) {
529
567
  delete settings.hooks;
@@ -534,10 +572,21 @@ function removeFeynmanHooks(settings) {
534
572
  function bootstrapState(target) {
535
573
  const cfg = targetConfig(target);
536
574
  fs.mkdirSync(cfg.feynmanDir, { recursive: true });
575
+ let state = DEFAULT_STATE;
537
576
  if (!fs.existsSync(cfg.statePath)) {
538
577
  fs.writeFileSync(cfg.statePath, JSON.stringify(DEFAULT_STATE, null, 2) + '\n');
578
+ } else {
579
+ try {
580
+ state = { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(cfg.statePath, 'utf8')) };
581
+ } catch (_) {
582
+ fs.writeFileSync(cfg.statePath, JSON.stringify(DEFAULT_STATE, null, 2) + '\n');
583
+ }
584
+ }
585
+ if (state.enabled) {
586
+ fs.writeFileSync(cfg.flagPath, state.intensity || DEFAULT_STATE.intensity);
587
+ } else if (fs.existsSync(cfg.flagPath)) {
588
+ fs.unlinkSync(cfg.flagPath);
539
589
  }
540
- fs.writeFileSync(cfg.flagPath, DEFAULT_STATE.intensity);
541
590
  }
542
591
 
543
592
  function installClaudeCommand() {
@@ -561,6 +610,7 @@ function installOne(target, opts) {
561
610
  // Read or create settings
562
611
  const cfg = readSettings(target);
563
612
  cfg.hooks = cfg.hooks || {};
613
+ cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
564
614
  cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
565
615
 
566
616
  // Idempotency check
@@ -572,20 +622,36 @@ function installOne(target, opts) {
572
622
  return { target, already: true };
573
623
  }
574
624
 
575
- // If force + already, remove old entry first
625
+ // If force or partial legacy install, remove old feynman entries first.
576
626
  if (already && force) {
577
627
  removeFeynmanHooks(cfg);
578
628
  cfg.hooks = cfg.hooks || {};
629
+ cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
630
+ cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
631
+ } else if (!already) {
632
+ removeFeynmanHooks(cfg);
633
+ cfg.hooks = cfg.hooks || {};
634
+ cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
579
635
  cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
580
636
  }
581
637
 
582
- // Append hook entry
638
+ // Append hook entries
639
+ const sessionEntry = {
640
+ hooks: [{
641
+ type: 'command',
642
+ command: hookCommandFor(target).replace(HOOK_PATH, SESSION_HOOK_PATH),
643
+ timeout: 5,
644
+ }]
645
+ };
646
+ if (target === 'codex') {
647
+ sessionEntry.matcher = 'startup|resume';
648
+ }
649
+ cfg.hooks.SessionStart.push(sessionEntry);
583
650
  cfg.hooks.UserPromptSubmit.push({
584
651
  hooks: [{
585
652
  type: 'command',
586
653
  command: hookCommandFor(target),
587
654
  timeout: 5,
588
- statusMessage: 'Injecting diagram rules...',
589
655
  }]
590
656
  });
591
657
 
@@ -623,7 +689,7 @@ function cmdInstall(opts) {
623
689
  }
624
690
  console.log('└──────────────────────────────────────────────────────────────┘');
625
691
  console.log('');
626
- console.log('Restart Claude Code or Codex to activate feynman.');
692
+ console.log('Restart Claude Code or Codex to activate feynman full mode.');
627
693
 
628
694
  process.exit(0);
629
695
  }
@@ -638,7 +704,7 @@ function uninstallOne(target) {
638
704
  }
639
705
 
640
706
  const cfg = readSettings(target);
641
- const hadHook = hasFeynmanHook(cfg);
707
+ const hadHook = hasAnyFeynmanHook(cfg);
642
708
  removeFeynmanHooks(cfg);
643
709
  writeSettings(target, cfg);
644
710
 
@@ -684,40 +750,54 @@ function cmdDoctor(opts = {}) {
684
750
  const settingsExists = fs.existsSync(tc.settingsPath);
685
751
  check(`${tc.settingsPath.replace(HOME, '~')} present`, settingsExists);
686
752
 
687
- // 2. UserPromptSubmit hook references feynman-activate.js
753
+ // 2. SessionStart and UserPromptSubmit hooks reference feynman scripts
754
+ let sessionHookRegistered = false;
688
755
  let hookRegistered = false;
756
+ let sessionHookAbsPath = null;
689
757
  let hookAbsPath = null;
690
758
  if (settingsExists) {
691
759
  const cfg = readSettings(target);
760
+ const sessionEntries = (cfg.hooks && cfg.hooks.SessionStart) || [];
761
+ const feynmanSessionEntry = sessionEntries.find(g =>
762
+ g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-session-start.js'))
763
+ );
764
+ sessionHookRegistered = !!feynmanSessionEntry;
765
+ if (feynmanSessionEntry) {
766
+ const hookCmd = feynmanSessionEntry.hooks.find(h => h.command && h.command.includes('feynman-session-start.js')).command;
767
+ sessionHookAbsPath = extractHookScriptPath(hookCmd, 'feynman-session-start.js');
768
+ }
769
+
692
770
  const entries = (cfg.hooks && cfg.hooks.UserPromptSubmit) || [];
693
771
  const feynmanEntry = entries.find(g =>
694
772
  g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
695
773
  );
696
774
  hookRegistered = !!feynmanEntry;
697
775
  if (feynmanEntry) {
698
- // Extract the path from command: node "/abs/path/to/feynman-activate.js"
699
776
  const hookCmd = feynmanEntry.hooks.find(h => h.command && h.command.includes('feynman-activate.js')).command;
700
- const match = hookCmd.match(/"([^"]+feynman-activate\.js)"/);
701
- if (match) hookAbsPath = match[1];
777
+ hookAbsPath = extractHookScriptPath(hookCmd, 'feynman-activate.js');
702
778
  }
703
779
  }
780
+ check('hook registered (feynman-session-start.js in SessionStart)', sessionHookRegistered);
704
781
  check('hook registered (feynman-activate.js in UserPromptSubmit)', hookRegistered);
705
782
 
706
- // 3. Hook script file exists + readable
783
+ // 3. Hook script files exist + readable
784
+ let sessionHookFileOk = false;
785
+ if (sessionHookAbsPath) {
786
+ try {
787
+ fs.accessSync(sessionHookAbsPath, fs.constants.R_OK);
788
+ sessionHookFileOk = true;
789
+ } catch (_) {}
790
+ }
791
+ check('session hook script file exists and is readable', sessionHookFileOk);
792
+
707
793
  let hookFileOk = false;
708
794
  if (hookAbsPath) {
709
795
  try {
710
796
  fs.accessSync(hookAbsPath, fs.constants.R_OK);
711
797
  hookFileOk = true;
712
798
  } catch (_) {}
713
- } else if (hookRegistered) {
714
- // Hook registered but path extraction failed — check default location
715
- try {
716
- fs.accessSync(HOOK_PATH, fs.constants.R_OK);
717
- hookFileOk = true;
718
- } catch (_) {}
719
799
  }
720
- check('hook script file exists and is readable', hookFileOk);
800
+ check('prompt hook script file exists and is readable', hookFileOk);
721
801
 
722
802
  // 4. Rules file exists + non-empty
723
803
  let rulesOk = false;
@@ -729,15 +809,20 @@ function cmdDoctor(opts = {}) {
729
809
 
730
810
  // 5. state.json valid JSON + has enabled field
731
811
  let stateOk = false;
812
+ let stateEnabled = false;
732
813
  try {
733
814
  const state = JSON.parse(fs.readFileSync(tc.statePath, 'utf8'));
734
815
  stateOk = 'enabled' in state;
816
+ stateEnabled = state.enabled === true;
735
817
  } catch (_) {}
736
818
  check('state.json valid (has enabled field)', stateOk);
737
819
 
738
- // 6. .feynman-active flag present
820
+ // 6. .feynman-active flag matches state
739
821
  const flagPresent = fs.existsSync(tc.flagPath);
740
- check('.feynman-active flag present', flagPresent);
822
+ check(
823
+ stateEnabled ? '.feynman-active flag present when enabled' : '.feynman-active flag absent when disabled',
824
+ stateEnabled ? flagPresent : !flagPresent
825
+ );
741
826
 
742
827
  // 7. (INFO) lint hook registered
743
828
  let lintHookRegistered = false;
@@ -6,16 +6,18 @@ Three independent layers: hook lifecycle, lint pipeline, and state schema.
6
6
 
7
7
  ## Layer 1: Hook Lifecycle
8
8
 
9
- The `UserPromptSubmit` hook fires before every Claude Code or Codex prompt.
10
- feynman intercepts the event, reads the active rules, and injects them as
11
- `additionalContext`.
9
+ The `SessionStart` hook primes fresh Claude Code or Codex sessions with the
10
+ active rules. The `UserPromptSubmit` hook fires before every prompt and
11
+ reinforces the same rules as `additionalContext`.
12
12
 
13
13
  ```
14
14
  ~/.claude/settings.json ~/.codex/hooks.json
15
15
 
16
- hooks.UserPromptSubmit fires on every prompt
16
+ ├─ hooks.SessionStart primes new sessions
17
+
18
+ └─ hooks.UserPromptSubmit reinforces every prompt
17
19
 
18
- hooks/feynman-activate.js
20
+ hooks/feynman-session-start.js + hooks/feynman-activate.js
19
21
 
20
22
  ├─ [0] FEYNMAN_HOME selects client state root
21
23
  │ unset → ~/.claude (backward compatible)
@@ -25,9 +27,10 @@ hooks/feynman-activate.js
25
27
  ├─ [1] validate session_id (path-traversal guard)
26
28
 
27
29
  ├─ [2] $FEYNMAN_HOME/.feynman-active ← flag file
28
- │ absent + no state.json → bootstrap first run
29
- │ absent + state.json exit 0 (user disabled)
30
- present continue
30
+ │ absent + no state.json → bootstrap first run
31
+ │ absent + state.enabled=true recreate flag
32
+ absent + state.enabled=false exit 0 (user disabled)
33
+ │ present → continue
31
34
 
32
35
  ├─ [3] $FEYNMAN_HOME/.feynman/state.json
33
36
  │ enabled: false → exit 0
@@ -121,7 +124,7 @@ All runtime state lives in two files under the selected client root:
121
124
  ~/.claude/ or ~/.codex/
122
125
  ├── .feynman-active ← presence flag
123
126
  │ present = feynman active
124
- │ absent = user disabled (state.json preserved)
127
+ │ absent = user disabled only when state.enabled=false
125
128
  │ content = current intensity string (informational)
126
129
 
127
130
  └── .feynman/
@@ -31,18 +31,25 @@ process.stdin.on('end', () => {
31
31
  if (sessionId && /[/\\]|\.\./.test(sessionId)) process.exit(0);
32
32
 
33
33
  // Step 2: flag file + first-run bootstrap (D-05, D-07, bug #35713)
34
- // True first run: neither flag nor state exists bootstrap both, then fall through
35
- // Intentionally disabled: flag absent but state exists exit 0 (user ran /feynman off)
34
+ // True first run: neither flag nor state exists -> bootstrap default full mode.
35
+ // Intentionally disabled: flag absent + state.enabled=false -> exit 0.
36
36
  const flagExists = fs.existsSync(FLAG_PATH);
37
37
  const stateExists = fs.existsSync(STATE_PATH);
38
38
  if (!flagExists) {
39
39
  if (!stateExists) {
40
- // First install bootstrap everything and activate
40
+ // First install: bootstrap and activate full mode.
41
41
  fs.mkdirSync(FEYNMAN_DIR, { recursive: true });
42
42
  fs.writeFileSync(STATE_PATH, JSON.stringify(DEFAULT_STATE, null, 2));
43
43
  fs.writeFileSync(FLAG_PATH, DEFAULT_STATE.intensity);
44
44
  } else {
45
- process.exit(0); // disabled intentionally by user
45
+ let existingState;
46
+ try {
47
+ existingState = { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) };
48
+ } catch (_) {
49
+ process.exit(0);
50
+ }
51
+ if (!existingState.enabled) process.exit(0);
52
+ fs.writeFileSync(FLAG_PATH, existingState.intensity || DEFAULT_STATE.intensity);
46
53
  }
47
54
  }
48
55
 
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // feynman — SessionStart hook — injects active diagram rules at session start.
3
+ // UserPromptSubmit still reinforces rules every turn; this primes fresh sessions.
4
+ 'use strict';
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const HOME = os.homedir();
11
+ const CLIENT_HOME = process.env.FEYNMAN_HOME || path.join(HOME, '.claude');
12
+ const FEYNMAN_DIR = path.join(CLIENT_HOME, '.feynman');
13
+ const STATE_PATH = path.join(FEYNMAN_DIR, 'state.json');
14
+ const FLAG_PATH = path.join(CLIENT_HOME, '.feynman-active');
15
+ const RULES_PATH = path.join(__dirname, '..', 'rules', 'feynman-activate.md');
16
+
17
+ const DEFAULT_STATE = { enabled: true, intensity: 'full', injections: 0 };
18
+ const VALID_INTENSITIES = ['lite', 'full', 'ultra'];
19
+
20
+ function writeState(state) {
21
+ fs.mkdirSync(FEYNMAN_DIR, { recursive: true });
22
+ fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
23
+ }
24
+
25
+ function readRules(intensity) {
26
+ const rulesContent = fs.readFileSync(RULES_PATH, 'utf8');
27
+ const selected = VALID_INTENSITIES.includes(intensity) ? intensity : 'full';
28
+ const openMarker = '<!-- ' + selected + ' -->';
29
+ const closeMarker = '<!-- /' + selected + ' -->';
30
+ const i1 = rulesContent.indexOf(openMarker);
31
+ const i2 = rulesContent.indexOf(closeMarker, i1);
32
+ if (i1 === -1 || i2 === -1) return '';
33
+ return rulesContent.slice(i1 + openMarker.length, i2).trim();
34
+ }
35
+
36
+ let input = '';
37
+ process.stdin.on('data', chunk => { input += chunk; });
38
+ process.stdin.on('end', () => {
39
+ try {
40
+ if (input.trim()) {
41
+ const data = JSON.parse(input);
42
+ const sessionId = data.session_id || '';
43
+ if (sessionId && /[/\\]|\.\./.test(sessionId)) process.exit(0);
44
+ }
45
+
46
+ const stateExists = fs.existsSync(STATE_PATH);
47
+ const flagExists = fs.existsSync(FLAG_PATH);
48
+ let state = { ...DEFAULT_STATE };
49
+
50
+ if (stateExists) {
51
+ try {
52
+ state = { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) };
53
+ } catch (_) {
54
+ try { fs.unlinkSync(FLAG_PATH); } catch (_) {}
55
+ process.exit(0);
56
+ }
57
+ } else {
58
+ writeState(state);
59
+ }
60
+
61
+ if (!state.enabled) {
62
+ try { fs.unlinkSync(FLAG_PATH); } catch (_) {}
63
+ process.exit(0);
64
+ }
65
+
66
+ if (!flagExists) {
67
+ fs.writeFileSync(FLAG_PATH, state.intensity || DEFAULT_STATE.intensity);
68
+ }
69
+
70
+ const rulesText = readRules(state.intensity);
71
+ if (!rulesText) process.exit(0);
72
+
73
+ // SessionStart accepts plain stdout as context, matching caveman's hook shape.
74
+ process.stdout.write(rulesText);
75
+ } catch (_) {
76
+ process.exit(0);
77
+ }
78
+ });
package/hooks/hooks.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "description": "Inject feynman ASCII diagram rules before each Claude Code prompt.",
3
3
  "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "FEYNMAN_HOME=\"$HOME/.claude\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/feynman-session-start.js\"",
10
+ "timeout": 5
11
+ }
12
+ ]
13
+ }
14
+ ],
4
15
  "UserPromptSubmit": [
5
16
  {
6
17
  "hooks": [
7
18
  {
8
19
  "type": "command",
9
20
  "command": "FEYNMAN_HOME=\"$HOME/.claude\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/feynman-activate.js\"",
10
- "timeout": 5,
11
- "statusMessage": "Injecting diagram rules..."
21
+ "timeout": 5
12
22
  }
13
23
  ]
14
24
  }
package/hooks.json CHANGED
@@ -1,13 +1,24 @@
1
1
  {
2
2
  "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|resume",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "PLUGIN_ROOT=\"${PLUGIN_ROOT:-$CLAUDE_PLUGIN_ROOT}\"; FEYNMAN_HOME=\"$HOME/.codex\" node \"$PLUGIN_ROOT/hooks/feynman-session-start.js\"",
10
+ "timeout": 5
11
+ }
12
+ ]
13
+ }
14
+ ],
3
15
  "UserPromptSubmit": [
4
16
  {
5
17
  "hooks": [
6
18
  {
7
19
  "type": "command",
8
20
  "command": "PLUGIN_ROOT=\"${PLUGIN_ROOT:-$CLAUDE_PLUGIN_ROOT}\"; FEYNMAN_HOME=\"$HOME/.codex\" node \"$PLUGIN_ROOT/hooks/feynman-activate.js\"",
9
- "timeout": 5,
10
- "statusMessage": "Injecting diagram rules..."
21
+ "timeout": 5
11
22
  }
12
23
  ]
13
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@albinocrabs/feynman",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Claude Code and Codex plugin that auto-injects ASCII diagram rules into every AI request via UserPromptSubmit hook",
5
5
  "license": "MIT",
6
6
  "author": "apolenkov",
@@ -111,6 +111,168 @@ Syntax:
111
111
  item-3
112
112
  ```
113
113
 
114
+ ### Terminal-safe rendering
115
+
116
+ Tables are allowed when they stay readable in terminal chat. Wide Markdown
117
+ tables are a rendering defect: convert them into a cleaner terminal layout
118
+ instead of letting columns wrap unpredictably.
119
+
120
+ Choose the layout by readability:
121
+
122
+ ```text
123
+ short matrix -> Markdown table is OK
124
+ status maps -> ASCII frame blocks
125
+ long rows -> key-value bullets
126
+ hierarchy -> ASCII tree
127
+ sequence -> arrow flow
128
+ comparison -> max 3 columns, max 10 words per cell
129
+ ```
130
+
131
+ Frame discipline:
132
+
133
+ ```text
134
+ +---- Status ----+
135
+ | item-a | done |
136
+ | item-b | risk |
137
+ +----------------+
138
+ ```
139
+
140
+ - Keep frame rows visually aligned.
141
+ - Keep diagram lines under about 88 columns when possible.
142
+ - If a row becomes long, split it into bullets or grouped frames instead of
143
+ widening the table.
144
+ - Optimize for human scanning: aligned labels, short cells, stable columns,
145
+ and no wrapped table rows.
146
+ - Use plain ASCII frames with `+`, `-`, and `|` when terminal compatibility
147
+ matters.
148
+
149
+ ### SDLC output patterns
150
+
151
+ For engineering status, retrospectives, handoffs, reviews, and release notes,
152
+ choose a human-scannable shape before writing prose. Prefer compact blocks with
153
+ explicit evidence over long narrative.
154
+
155
+ Use these shapes:
156
+
157
+ ```text
158
+ status -> frame with state, branch, commit, checks, blocker
159
+ retro -> DONE / WORKED / FRAGILE / LEFT
160
+ handoff -> NOW / NEXT / FILES / COMMANDS / RISK
161
+ review -> FINDINGS first, then QUESTIONS, then SUMMARY
162
+ incident -> IMPACT / CAUSE / FIX / PREVENTION
163
+ release -> CHANGED / VERIFIED / RISK / ROLLBACK
164
+ decision -> CONTEXT / OPTIONS / CHOICE / CONSEQUENCE
165
+ verification -> command -> result -> evidence -> gap
166
+ roadmap -> NOW / NEXT / LATER / BLOCKED
167
+ phase -> GOAL / SCOPE / PLAN / VERIFY / EXIT
168
+ UAT -> SCENARIO / EXPECTED / ACTUAL / RESULT
169
+ risk register -> RISK / IMPACT / MITIGATION / OWNER
170
+ ```
171
+
172
+ Status block pattern:
173
+
174
+ ```text
175
+ +---- Status ----+
176
+ | repo | name |
177
+ | branch | main |
178
+ | commit | abc12 |
179
+ | checks | PASS |
180
+ | blocker | none |
181
+ +----------------+
182
+ ```
183
+
184
+ Retro pattern:
185
+
186
+ ```text
187
+ DONE:
188
+ - landed change with evidence
189
+
190
+ WORKED:
191
+ - useful command or decision
192
+
193
+ FRAGILE:
194
+ - risk or assumption
195
+
196
+ LEFT:
197
+ - next executable action
198
+ ```
199
+
200
+ Roadmap pattern:
201
+
202
+ ```text
203
+ NOW:
204
+ - current milestone or active phase
205
+
206
+ NEXT:
207
+ - next executable phase
208
+
209
+ LATER:
210
+ - deferred work
211
+
212
+ BLOCKED:
213
+ - dependency or decision needed
214
+ ```
215
+
216
+ Phase pattern:
217
+
218
+ ```text
219
+ GOAL:
220
+ - promised outcome
221
+
222
+ SCOPE:
223
+ - included / excluded boundaries
224
+
225
+ PLAN:
226
+ - implementation path
227
+
228
+ VERIFY:
229
+ - command, test, or review evidence
230
+
231
+ EXIT:
232
+ - condition for done
233
+ ```
234
+
235
+ UAT pattern:
236
+
237
+ ```text
238
+ SCENARIO:
239
+ - user action or workflow
240
+
241
+ EXPECTED:
242
+ - expected behavior
243
+
244
+ ACTUAL:
245
+ - observed behavior
246
+
247
+ RESULT:
248
+ - PASS / FAIL / BLOCKED
249
+ ```
250
+
251
+ Risk register pattern:
252
+
253
+ ```text
254
+ ▲ high
255
+ RISK:
256
+ - what can go wrong
257
+ IMPACT:
258
+ - why it matters
259
+ MITIGATION:
260
+ - concrete control
261
+ OWNER:
262
+ - agent / human / system
263
+ ▼ low
264
+ ```
265
+
266
+ Rules:
267
+
268
+ - Put the answer first, evidence second, next action last.
269
+ - Never bury blockers in prose; give them their own label.
270
+ - For status and retro, avoid wide tables even when Markdown would be valid.
271
+ - Prefer `PASS`, `FAIL`, `BLOCKED`, `not run`, and exact command names.
272
+ - If verification was not run, say `not run` and name the command.
273
+ - For Russian chat, keep prose Russian and keep commands, paths, config keys,
274
+ commits, and status labels in English.
275
+
114
276
  ### When no diagram appears
115
277
 
116
278
  Responses that are any of the following contain no diagram:
@@ -154,13 +316,74 @@ detail | detail
154
316
 
155
317
  **Comparisons** — Same as full mode. Includes side-by-side ASCII columns.
156
318
 
157
- **Status summaries** — Same as full mode. Uses ┌─ frame blocks.
319
+ **Status summaries** — Same as full mode. Uses ASCII `+---` frame blocks.
158
320
 
159
321
  **Priority orderings** — Same as full mode. Uses ▲▼ scale.
160
322
 
323
+ ### Terminal-safe rendering
324
+
325
+ Tables are allowed when they are compact and readable. Wide Markdown tables are
326
+ a rendering defect in terminal chat. Prefer ASCII frames, key-value bullets,
327
+ trees, and short columns for long content. Keep diagram lines under about 88
328
+ columns when possible. If content is long, split it into bullets or grouped
329
+ frames instead of widening the visual block.
330
+
331
+ ### SDLC output patterns
332
+
333
+ All full-mode SDLC patterns apply. In ultra mode, use them aggressively for any
334
+ engineering status, retrospective, review, release, handoff, decision, or
335
+ verification answer. The default shape is:
336
+
337
+ ```text
338
+ [state] --> [evidence] --> [risk] --> [next]
339
+ ```
340
+
341
+ For retrospectives, prefer:
342
+
343
+ ```text
344
+ DONE / WORKED / FRAGILE / LEFT
345
+ ```
346
+
347
+ For status answers, prefer:
348
+
349
+ ```text
350
+ +---- Status ----+
351
+ | item | state |
352
+ | checks | PASS |
353
+ | blocker | none |
354
+ +----------------+
355
+ ```
356
+
161
357
  ### When no diagram appears
162
358
 
163
359
  The only response that contains no diagram is a single sentence of pure prose with no enumerable items, no steps, no comparisons, and no structure of any kind.
164
360
 
165
361
  All other responses — including those with two or more items, any named concept with sub-parts, any sequence of actions, any set of options — include an ASCII diagram.
166
362
  <!-- /ultra -->
363
+
364
+ ### Russian terminal chat visual guardrail
365
+
366
+ For Russian terminal chat, avoid using ASCII frames as two-column tables when
367
+ values contain long Cyrillic or mixed Cyrillic/Latin text. Visual alignment can
368
+ look broken even when character counts are correct.
369
+
370
+ Prefer bullets for long Russian status facts:
371
+
372
+ Итог:
373
+ - Что сделали: dtp-retro стал русским по умолчанию.
374
+ - История: текущая session ищется автоматически.
375
+ - Форма: сначала смысл, потом proof/details.
376
+ - Статус: правки есть, commit ещё не сделан.
377
+
378
+ Avoid frame rows like this for long Russian text:
379
+
380
+ | что сделали | длинная русская строка ... |
381
+ | история | длинная смешанная строка ... |
382
+
383
+ Rule:
384
+
385
+ - frame with short values -> OK
386
+ - long Russian value -> bullets
387
+ - sequence -> arrows
388
+ - comparison -> max 3 short columns
389
+ - status with long text -> label bullets, not frame rows
@@ -26,11 +26,21 @@ Parse `$ARGUMENTS`:
26
26
  ```bash
27
27
  node -e "
28
28
  const fs = require('fs'), os = require('os'), path = require('path');
29
- const stateFile = path.join(os.homedir(), '.claude', '.feynman', 'state.json');
30
- const flagFile = path.join(os.homedir(), '.claude', '.feynman-active');
31
- let st = {enabled: false, intensity: 'full', injections: 0};
29
+ function clientHome() {
30
+ if (process.env.FEYNMAN_HOME) return process.env.FEYNMAN_HOME;
31
+ if (process.env.FEYNMAN_TARGET === 'codex') return path.join(os.homedir(), '.codex');
32
+ if (process.env.FEYNMAN_TARGET === 'claude') return path.join(os.homedir(), '.claude');
33
+ if (process.env.CODEX_HOME) return process.env.CODEX_HOME;
34
+ if (process.env.CODEX_THREAD_ID || process.env.CODEX_SANDBOX) return path.join(os.homedir(), '.codex');
35
+ if (process.env.CLAUDE_CONFIG_DIR) return process.env.CLAUDE_CONFIG_DIR;
36
+ return path.join(os.homedir(), '.claude');
37
+ }
38
+ const root = clientHome();
39
+ const stateFile = path.join(root, '.feynman', 'state.json');
40
+ const flagFile = path.join(root, '.feynman-active');
41
+ let st = {enabled: true, intensity: 'full', injections: 0};
32
42
  try { st = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch(e) {}
33
- console.log('enabled:', st.enabled, '| intensity:', st.intensity, '| injections:', (st.injections ?? st.count ?? 0), '| flag:', fs.existsSync(flagFile));
43
+ console.log('target:', path.basename(root), '| enabled:', st.enabled, '| intensity:', st.intensity, '| injections:', (st.injections ?? st.count ?? 0), '| flag:', fs.existsSync(flagFile));
34
44
  "
35
45
  ```
36
46
 
@@ -39,18 +49,29 @@ console.log('enabled:', st.enabled, '| intensity:', st.intensity, '| injections:
39
49
  ```bash
40
50
  node -e "
41
51
  const fs = require('fs'), os = require('os'), path = require('path');
42
- const stateFile = path.join(os.homedir(), '.claude', '.feynman', 'state.json');
43
- const flagFile = path.join(os.homedir(), '.claude', '.feynman-active');
52
+ function clientHome() {
53
+ if (process.env.FEYNMAN_HOME) return process.env.FEYNMAN_HOME;
54
+ if (process.env.FEYNMAN_TARGET === 'codex') return path.join(os.homedir(), '.codex');
55
+ if (process.env.FEYNMAN_TARGET === 'claude') return path.join(os.homedir(), '.claude');
56
+ if (process.env.CODEX_HOME) return process.env.CODEX_HOME;
57
+ if (process.env.CODEX_THREAD_ID || process.env.CODEX_SANDBOX) return path.join(os.homedir(), '.codex');
58
+ if (process.env.CLAUDE_CONFIG_DIR) return process.env.CLAUDE_CONFIG_DIR;
59
+ return path.join(os.homedir(), '.claude');
60
+ }
61
+ const root = clientHome();
62
+ const stateFile = path.join(root, '.feynman', 'state.json');
63
+ const flagFile = path.join(root, '.feynman-active');
44
64
  const arg = (process.argv[1] || '').trim().toLowerCase();
45
65
  const normalized = arg === 'start' ? 'on' : arg === 'stop' ? 'off' : arg;
46
- let st = {enabled: true, intensity: 'full', injections: 0};
66
+ let st = {enabled: false, intensity: 'full', injections: 0};
47
67
  try { st = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch(e) {}
48
- if (normalized === 'on') { st.enabled = true; fs.writeFileSync(flagFile, st.intensity); }
68
+ function writeFlag(value) { fs.mkdirSync(root, {recursive: true}); fs.writeFileSync(flagFile, value || 'full'); }
69
+ if (normalized === 'on') { st.enabled = true; writeFlag(st.intensity); }
49
70
  if (normalized === 'off') { st.enabled = false; try { fs.unlinkSync(flagFile); } catch(e) {} }
50
- if (['lite','full','ultra'].includes(normalized)) { st.intensity = normalized; st.enabled = true; fs.writeFileSync(flagFile, normalized); }
71
+ if (['lite','full','ultra'].includes(normalized)) { st.intensity = normalized; st.enabled = true; writeFlag(normalized); }
51
72
  fs.mkdirSync(path.dirname(stateFile), {recursive: true});
52
73
  fs.writeFileSync(stateFile, JSON.stringify(st, null, 2));
53
- console.log(JSON.stringify(st));
74
+ console.log(JSON.stringify({target: path.basename(root), ...st}));
54
75
  " "$ARGUMENTS"
55
76
  ```
56
77