@albinocrabs/feynman 0.2.5 → 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.5",
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,36 @@
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
+
5
35
  ## 0.2.5 - 2026-05-08
6
36
 
7
37
  ### Features
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,6 +195,18 @@ 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": [
@@ -214,6 +228,18 @@ For Codex, add the same shape to `~/.codex/hooks.json` and set
214
228
  ```json
215
229
  {
216
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
+ ],
217
243
  "UserPromptSubmit": [
218
244
  {
219
245
  "hooks": [
@@ -234,6 +260,37 @@ After install, feynman starts in `full` mode by default. Disable or change it
234
260
  explicitly with `/feynman off`, `/feynman lite`, `/feynman full`, or
235
261
  `/feynman ultra`.
236
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
+
237
294
  ## Intensity Levels
238
295
 
239
296
  | Level | What draws | Use when |
@@ -278,6 +335,26 @@ This bypasses `~/.codex/hooks.json` hook execution entirely.
278
335
  Regular `/feynman off` and `/feynman on` continue to use normal profile state
279
336
  files (`~/.codex/.feynman-active`, `~/.codex/.feynman/state.json`).
280
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
+
281
358
  ## CLI examples
282
359
 
283
360
  Quickly discover and view repository prompt templates:
@@ -492,15 +569,42 @@ the rules for the active intensity level, and inject them into model context.
492
569
  ```
493
570
  [your prompt]
494
571
  +
495
- [feynman rules] injected by hook, ~2KB
572
+ [feynman rules] injected by hook
496
573
 
497
574
 
498
- [Claude]
575
+ [Claude Code]
576
+ or
577
+ [Codex]
499
578
 
500
579
 
501
580
  [structured response with ASCII diagrams]
502
581
  ```
503
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
+
504
608
  ## Release process
505
609
 
506
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
@@ -109,7 +109,7 @@ ${c.bold('Options:')}
109
109
  ${c.bold('Examples:')}
110
110
  npx @albinocrabs/feynman install
111
111
  npx @albinocrabs/feynman install --target codex
112
- npx @albinocrabs/feynman install --target both
112
+ npx @albinocrabs/feynman install --target all
113
113
  npx @albinocrabs/feynman install --target all
114
114
  npx @albinocrabs/feynman doctor
115
115
  feynman lint response.md
@@ -507,6 +507,14 @@ function writeSettings(target, settings) {
507
507
  fs.writeFileSync(cfg.settingsPath, JSON.stringify(settings, null, 2) + '\n');
508
508
  }
509
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
+
510
518
  function hasFeynmanHook(settings) {
511
519
  const promptHook = ((settings.hooks && settings.hooks.UserPromptSubmit) || []).some(g =>
512
520
  g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
@@ -517,19 +525,40 @@ function hasFeynmanHook(settings) {
517
525
  return promptHook && sessionHook;
518
526
  }
519
527
 
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
+ )
534
+ );
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
+
520
549
  function removeFeynmanHooks(settings) {
521
550
  if (!settings.hooks) return settings;
522
551
  for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
523
552
  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
- );
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);
533
562
  if (settings.hooks[eventName].length === 0) {
534
563
  delete settings.hooks[eventName];
535
564
  }
@@ -675,7 +704,7 @@ function uninstallOne(target) {
675
704
  }
676
705
 
677
706
  const cfg = readSettings(target);
678
- const hadHook = hasFeynmanHook(cfg);
707
+ const hadHook = hasAnyFeynmanHook(cfg);
679
708
  removeFeynmanHooks(cfg);
680
709
  writeSettings(target, cfg);
681
710
 
@@ -735,8 +764,7 @@ function cmdDoctor(opts = {}) {
735
764
  sessionHookRegistered = !!feynmanSessionEntry;
736
765
  if (feynmanSessionEntry) {
737
766
  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];
767
+ sessionHookAbsPath = extractHookScriptPath(hookCmd, 'feynman-session-start.js');
740
768
  }
741
769
 
742
770
  const entries = (cfg.hooks && cfg.hooks.UserPromptSubmit) || [];
@@ -745,10 +773,8 @@ function cmdDoctor(opts = {}) {
745
773
  );
746
774
  hookRegistered = !!feynmanEntry;
747
775
  if (feynmanEntry) {
748
- // Extract the path from command: node "/abs/path/to/feynman-activate.js"
749
776
  const hookCmd = feynmanEntry.hooks.find(h => h.command && h.command.includes('feynman-activate.js')).command;
750
- const match = hookCmd.match(/"([^"]+feynman-activate\.js)"/);
751
- if (match) hookAbsPath = match[1];
777
+ hookAbsPath = extractHookScriptPath(hookCmd, 'feynman-activate.js');
752
778
  }
753
779
  }
754
780
  check('hook registered (feynman-session-start.js in SessionStart)', sessionHookRegistered);
@@ -761,11 +787,6 @@ function cmdDoctor(opts = {}) {
761
787
  fs.accessSync(sessionHookAbsPath, fs.constants.R_OK);
762
788
  sessionHookFileOk = true;
763
789
  } catch (_) {}
764
- } else if (sessionHookRegistered) {
765
- try {
766
- fs.accessSync(SESSION_HOOK_PATH, fs.constants.R_OK);
767
- sessionHookFileOk = true;
768
- } catch (_) {}
769
790
  }
770
791
  check('session hook script file exists and is readable', sessionHookFileOk);
771
792
 
@@ -775,12 +796,6 @@ function cmdDoctor(opts = {}) {
775
796
  fs.accessSync(hookAbsPath, fs.constants.R_OK);
776
797
  hookFileOk = true;
777
798
  } catch (_) {}
778
- } else if (hookRegistered) {
779
- // Hook registered but path extraction failed — check default location
780
- try {
781
- fs.accessSync(HOOK_PATH, fs.constants.R_OK);
782
- hookFileOk = true;
783
- } catch (_) {}
784
799
  }
785
800
  check('prompt hook script file exists and is readable', hookFileOk);
786
801
 
@@ -17,14 +17,6 @@ const RULES_PATH = path.join(__dirname, '..', 'rules', 'feynman-activate.md');
17
17
  const DEFAULT_STATE = { enabled: true, intensity: 'full', injections: 0 };
18
18
  const VALID_INTENSITIES = ['lite', 'full', 'ultra'];
19
19
 
20
- function readState() {
21
- try {
22
- return { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) };
23
- } catch (_) {
24
- return { ...DEFAULT_STATE };
25
- }
26
- }
27
-
28
20
  function writeState(state) {
29
21
  fs.mkdirSync(FEYNMAN_DIR, { recursive: true });
30
22
  fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
@@ -53,9 +45,16 @@ process.stdin.on('end', () => {
53
45
 
54
46
  const stateExists = fs.existsSync(STATE_PATH);
55
47
  const flagExists = fs.existsSync(FLAG_PATH);
56
- const state = readState();
48
+ let state = { ...DEFAULT_STATE };
57
49
 
58
- if (!stateExists) {
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 {
59
58
  writeState(state);
60
59
  }
61
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@albinocrabs/feynman",
3
- "version": "0.2.5",
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');
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');
31
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
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