@north-light/crouter 0.3.162 → 0.3.163

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.
Files changed (95) hide show
  1. package/dist/builtin-memory/00-runtime-base.md +1 -0
  2. package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
  3. package/dist/builtin-memory/init.md +38 -0
  4. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
  6. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +34 -2
  7. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +14 -3
  8. package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
  9. package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
  10. package/dist/clients/attach/__tests__/edit-diff.test.js +34 -0
  11. package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
  12. package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
  13. package/dist/clients/attach/render/chat-view.js +8 -3
  14. package/dist/clients/attach/render/crtr-output.js +31 -2
  15. package/dist/clients/attach/render/edit-diff.js +26 -2
  16. package/dist/clients/attach/render/tool-calls.d.ts +7 -2
  17. package/dist/clients/attach/render/tool-calls.js +25 -4
  18. package/dist/clients/attach/session/connection.d.ts +1 -1
  19. package/dist/clients/attach/session/editor-frame.d.ts +5 -1
  20. package/dist/clients/attach/session/editor-frame.js +21 -15
  21. package/dist/clients/attach/session/identity.d.ts +1 -1
  22. package/dist/clients/attach/viewer.js +621 -619
  23. package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
  24. package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
  25. package/dist/commands/__tests__/human.test.js +2 -0
  26. package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
  27. package/dist/commands/__tests__/search-contents.test.js +52 -0
  28. package/dist/commands/api-client.js +1 -0
  29. package/dist/commands/human/prompts.js +2 -2
  30. package/dist/commands/human/shared.d.ts +1 -0
  31. package/dist/commands/human/shared.js +7 -4
  32. package/dist/commands/memory/lint.d.ts +6 -0
  33. package/dist/commands/memory/lint.js +177 -35
  34. package/dist/commands/memory/read.js +22 -1
  35. package/dist/commands/memory/write.js +18 -4
  36. package/dist/commands/memory.js +1 -1
  37. package/dist/commands/search/contents.js +2 -2
  38. package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
  39. package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
  40. package/dist/commands/sys/setup-wizard.d.ts +8 -1
  41. package/dist/commands/sys/setup-wizard.js +209 -58
  42. package/dist/commands/sys/sync-deps.js +14 -18
  43. package/dist/commands/sys/sync-project-guidance.js +16 -19
  44. package/dist/core/__tests__/migration.test.js +8 -3
  45. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
  46. package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
  47. package/dist/core/__tests__/on-read-identity.test.js +8 -15
  48. package/dist/core/__tests__/revive.test.js +2 -2
  49. package/dist/core/__tests__/tmux-surface.test.js +10 -4
  50. package/dist/core/__tests__/worktree.test.js +3 -3
  51. package/dist/core/canvas/extensions.d.ts +1 -1
  52. package/dist/core/canvas/extensions.js +18 -11
  53. package/dist/core/canvas/labels.d.ts +4 -5
  54. package/dist/core/canvas/labels.js +9 -9
  55. package/dist/core/command-manifests/schema.js +18 -5
  56. package/dist/core/command.js +17 -7
  57. package/dist/core/configured-clis/invoker.js +2 -0
  58. package/dist/core/help.d.ts +3 -1
  59. package/dist/core/help.js +5 -2
  60. package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
  61. package/dist/core/keybindings/catalog.d.ts +3 -2
  62. package/dist/core/keybindings/catalog.js +34 -25
  63. package/dist/core/keybindings/index.d.ts +1 -1
  64. package/dist/core/keybindings/resolve.js +8 -0
  65. package/dist/core/keybindings/types.d.ts +6 -1
  66. package/dist/core/memory/doc-link-grammar.d.ts +20 -0
  67. package/dist/core/memory/doc-link-grammar.js +110 -0
  68. package/dist/core/memory-resolver.d.ts +5 -0
  69. package/dist/core/memory-resolver.js +12 -1
  70. package/dist/core/runtime/bearings.d.ts +8 -8
  71. package/dist/core/runtime/bearings.js +20 -16
  72. package/dist/core/runtime/broker.js +3 -13
  73. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  74. package/dist/core/runtime/canvas-extensions.js +2 -0
  75. package/dist/core/runtime/launch.d.ts +1 -1
  76. package/dist/core/runtime/launch.js +1 -1
  77. package/dist/core/runtime/tmux.js +136 -100
  78. package/dist/core/scope.js +4 -5
  79. package/dist/core/substrate/ceiling.d.ts +3 -6
  80. package/dist/core/substrate/ceiling.js +13 -15
  81. package/dist/core/substrate/index.d.ts +2 -3
  82. package/dist/core/substrate/index.js +2 -2
  83. package/dist/core/substrate/on-read.d.ts +4 -13
  84. package/dist/core/substrate/on-read.js +142 -262
  85. package/dist/core/substrate/render.js +4 -4
  86. package/dist/core/substrate/schema.d.ts +3 -2
  87. package/dist/core/substrate/schema.js +5 -5
  88. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
  89. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
  90. package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
  91. package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
  92. package/dist/pi-extensions/canvas-tool-guide.js +74 -0
  93. package/package.json +1 -1
  94. package/runtime.lock.json +2 -2
  95. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process';
11
11
  import { readFileSync, rmSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
13
13
  import { join } from 'node:path';
14
- import { formatGesture, resolveUserKeybindings, } from '../keybindings/index.js';
14
+ import { BINDING_CATALOG, formatGesture, resolveUserKeybindings, } from '../keybindings/index.js';
15
15
  import { bundledPiCommandForShell } from './pi-cli.js';
16
16
  import { surfaceTmuxStyleArgs } from './surface-bg.js';
17
17
  import { inboxToggleTmuxCommand } from '@crouton-kit/humanloop';
@@ -470,7 +470,7 @@ export function sendKeysEnter(pane, text) {
470
470
  // ---------------------------------------------------------------------------
471
471
  const TMUX_BINDING_MANIFEST = '@crtr_keybindings_manifest';
472
472
  const ROOT_TABLE = 'root';
473
- const MENU_ACTIONS = {
473
+ const LEAF_MENU_ACTIONS = {
474
474
  'crtr.tmux.menu.promote': { description: 'promote to orchestrator', action: { kind: 'keys', keys: '/promote' } },
475
475
  'crtr.tmux.menu.resume': { description: 'resume node', action: { kind: 'keys', keys: '/resume-node' } },
476
476
  'crtr.tmux.menu.fork': { description: 'fork into idle root', action: { kind: 'direct', run: 'node fork --pane {pane}' } },
@@ -484,7 +484,6 @@ const MENU_ACTIONS = {
484
484
  'crtr.tmux.menu.file-review': { description: 'review a file', action: { kind: 'raw-keys', keys: 'M-S-r' } },
485
485
  'crtr.tmux.menu.metadata': { description: 'metadata', action: { kind: 'keys', keys: '/node-metadata' } },
486
486
  'crtr.tmux.menu.fold-tools': { description: 'fold finished tool calls', action: { kind: 'keys', keys: '/fold-tools' } },
487
- 'crtr.tmux.menu.view-settings': undefined,
488
487
  'crtr.tmux.menu.context': { description: 'context + reports', action: { kind: 'keys', keys: '/context' } },
489
488
  'crtr.tmux.menu.providers': { description: 'settings', action: { kind: 'popup', run: 'sys setup' } },
490
489
  'crtr.tmux.menu.graph': { description: 'graph view', action: { kind: 'keys', keys: '/graph' } },
@@ -492,16 +491,8 @@ const MENU_ACTIONS = {
492
491
  'crtr.tmux.menu.copy-id': { description: 'copy node id', action: { kind: 'command', run: 'surface node id --pane {pane}' } },
493
492
  'crtr.tmux.menu.reload-viewer': { description: 'reload viewer', action: { kind: 'direct', run: 'surface reload --pane {pane}' } },
494
493
  'crtr.tmux.menu.pin': { description: 'pin conversation', action: { kind: 'direct', run: 'surface node pin --pane {pane}' } },
495
- 'crtr.tmux.menu.open': undefined,
496
- 'crtr.tmux.menu.show': undefined,
497
- 'crtr.tmux.menu.issues': undefined,
498
- 'crtr.tmux.menu.issue.idea': undefined,
499
- 'crtr.tmux.menu.issue.bug': undefined,
500
- 'crtr.tmux.node.next': undefined,
501
- 'crtr.tmux.node.previous': undefined,
502
- 'crtr.tmux.inbox.toggle': undefined,
503
- 'crtr.tmux.view.next': undefined,
504
- 'crtr.tmux.view.previous': undefined,
494
+ 'crtr.tmux.menu.issue.idea': { description: 'idea', action: { kind: 'popup', run: 'canvas issue new --type idea' } },
495
+ 'crtr.tmux.menu.issue.bug': { description: 'bug', action: { kind: 'popup', run: 'canvas issue new --type bug' } },
505
496
  };
506
497
  function escapeTmuxFormats(value) {
507
498
  return value.replace(/#/g, '##');
@@ -512,34 +503,69 @@ function tmuxCommand(args) {
512
503
  function chordCommand(run, interactive = false) {
513
504
  const command = `crtr canvas chord --pane '#{pane_id}' --run ${shellQuote(escapeTmuxFormats(run))}${interactive ? ' --interactive' : ''}`;
514
505
  return interactive
515
- ? `display-popup -E -w 60% -h 50% -d "#{pane_current_path}" ${shellQuote(command)}`
516
- : `run-shell ${shellQuote(`${command} >/dev/null 2>&1`)}`;
506
+ ? {
507
+ argv: ['display-popup', '-E', '-w', '60%', '-h', '50%', '-d', '#{pane_current_path}', command],
508
+ menu: `display-popup -E -w 60% -h 50% -d "#{pane_current_path}" ${shellQuote(command)}`,
509
+ }
510
+ : {
511
+ argv: ['run-shell', `${command} >/dev/null 2>&1`],
512
+ menu: `run-shell ${shellQuote(`${command} >/dev/null 2>&1`)}`,
513
+ };
517
514
  }
518
- /** Dispatch a menu action straight to a `crtr` invocation, skipping the
515
+ /** Dispatch an action straight to a `crtr` invocation, skipping the
519
516
  * `canvas chord` hop entirely. Only sound for an action whose sole
520
517
  * interpolation need is `{pane}` — tmux expands it natively as `#{pane_id}` at
521
- * selection time (a real tmux format, substituted AFTER escaping the static
518
+ * keypress time (a real tmux format, substituted AFTER escaping the static
522
519
  * run text so the `#` here is never doubled), so chord's node-resolution
523
- * round trip buys nothing. `-b` keeps the menu selection non-blocking. */
520
+ * round trip buys nothing. `-b` keeps dispatch non-blocking. */
524
521
  function directCommand(run) {
525
522
  const argv = escapeTmuxFormats(run).replace('{pane}', '#{pane_id}');
526
- return `run-shell -b ${shellQuote(`crtr ${argv} >/dev/null 2>&1`)}`;
523
+ const command = `crtr ${argv} >/dev/null 2>&1`;
524
+ return {
525
+ argv: ['run-shell', '-b', command],
526
+ menu: `run-shell -b ${shellQuote(command)}`,
527
+ };
527
528
  }
528
- function renderMenuAction(action) {
529
+ /** One command constructor serves both display-menu selections and direct root
530
+ * shortcuts. Each result carries the argv bind-key needs plus the stable menu
531
+ * serialization existing Alt+C installs already use. */
532
+ function menuActionCommand(action) {
529
533
  switch (action.kind) {
530
- case 'disabled': return '';
531
- case 'separator': return '';
534
+ case 'disabled': return null;
535
+ case 'separator': return null;
532
536
  // Choosing any menu item closes it; refresh-client is the deliberately
533
537
  // inert command that lets the prefix shortcut choose this close row.
534
- case 'dismiss': return 'refresh-client';
535
- case 'keys': return `send-keys -t '#{pane_id}' ${shellQuote(escapeTmuxFormats(action.keys))} Enter`;
536
- case 'raw-keys': return `send-keys -t '#{pane_id}' ${shellQuote(escapeTmuxFormats(action.keys))}`;
538
+ case 'dismiss': return { argv: ['refresh-client'], menu: 'refresh-client' };
539
+ case 'keys': {
540
+ const keys = escapeTmuxFormats(action.keys);
541
+ return {
542
+ // A root binding already runs in the pressed key's pane context. Its
543
+ // target must stay implicit: bind-key does not expand `#{pane_id}` in
544
+ // send-keys' target argument. A menu command is serialized later and
545
+ // does need the pane captured when the menu opened.
546
+ argv: ['send-keys', keys, 'Enter'],
547
+ menu: `send-keys -t '#{pane_id}' ${shellQuote(keys)} Enter`,
548
+ };
549
+ }
550
+ case 'raw-keys': {
551
+ const keys = escapeTmuxFormats(action.keys);
552
+ return {
553
+ argv: ['send-keys', keys],
554
+ menu: `send-keys -t '#{pane_id}' ${shellQuote(keys)}`,
555
+ };
556
+ }
537
557
  case 'command': return chordCommand(action.run);
538
558
  case 'direct': return directCommand(action.run);
539
559
  case 'popup': return chordCommand(action.run, true);
540
- case 'submenu': return tmuxCommand(buildMenuArgs(action.title, action.items));
560
+ case 'submenu': {
561
+ const argv = buildMenuArgs(action.title, action.items);
562
+ return { argv, menu: tmuxCommand(argv) };
563
+ }
541
564
  }
542
565
  }
566
+ function renderMenuAction(action) {
567
+ return menuActionCommand(action)?.menu ?? '';
568
+ }
543
569
  /** Visible width of a menu item name: style markup (`#[...]`) takes no cells. */
544
570
  function menuItemWidth(description) {
545
571
  return description.replace(/#\[[^\]]*\]/g, '').length;
@@ -573,8 +599,12 @@ function tmuxKey(stroke) {
573
599
  let key = parts.pop();
574
600
  const modifiers = new Set(parts);
575
601
  if (modifiers.has('shift') && key.length === 1 && /[a-z]/.test(key)) {
602
+ // tmux names a shifted printable with its uppercase character even when an
603
+ // extended-key terminal also preserves Shift as a modifier: Alt+Shift+F is
604
+ // `M-S-F`. Shift by itself needs only the uppercase character (`F`).
576
605
  key = key.toUpperCase();
577
- modifiers.delete('shift');
606
+ if (!modifiers.has('ctrl') && !modifiers.has('alt'))
607
+ modifiers.delete('shift');
578
608
  }
579
609
  key = TMUX_NAMED_KEYS[key] ?? key;
580
610
  const prefix = [
@@ -589,34 +619,56 @@ function menuSelector(stroke) {
589
619
  const key = parts.at(-1);
590
620
  return parts.includes('shift') && /[a-z]/.test(key) ? key.toUpperCase() : key === 'space' ? ' ' : key;
591
621
  }
592
- function menuItems(bindings) {
593
- const boundItems = (ids) => {
594
- const items = [];
595
- for (const id of ids) {
596
- const entry = MENU_ACTIONS[id];
597
- if (entry === undefined)
598
- continue;
599
- for (const gesture of bindings.gestures(id)) {
600
- items.push({ description: entry.description, selector: menuSelector(gesture), action: entry.action });
601
- }
602
- }
603
- return items;
604
- };
605
- const issueItems = [];
606
- for (const [id, description, type] of [
607
- ['crtr.tmux.menu.issue.idea', 'idea', 'idea'],
608
- ['crtr.tmux.menu.issue.bug', 'bug', 'bug'],
609
- ]) {
622
+ function boundMenuItems(ids, entries, bindings) {
623
+ const items = [];
624
+ for (const id of ids) {
625
+ const entry = entries.get(id);
626
+ if (entry === undefined)
627
+ continue;
610
628
  for (const gesture of bindings.gestures(id)) {
611
- issueItems.push({ description, selector: menuSelector(gesture), action: { kind: 'popup', run: `canvas issue new --type ${type}` } });
629
+ items.push({ description: entry.description, selector: menuSelector(gesture), action: entry.action });
612
630
  }
613
631
  }
614
- const lifecycleItems = boundItems([
615
- 'crtr.tmux.menu.promote',
616
- 'crtr.tmux.menu.demote',
617
- 'crtr.tmux.menu.detach',
618
- 'crtr.tmux.menu.close-subtree',
619
- ]);
632
+ return items;
633
+ }
634
+ /** Resolve the complete action descriptor set once. Submenu openers point at
635
+ * the same child MenuItems the prefix menu renders, so their direct global
636
+ * shortcuts cannot drift from the menu path. */
637
+ function resolvedMenuActions(bindings) {
638
+ const entries = new Map();
639
+ for (const [id, entry] of Object.entries(LEAF_MENU_ACTIONS)) {
640
+ if (entry !== undefined)
641
+ entries.set(id, entry);
642
+ }
643
+ const showItems = boundMenuItems([
644
+ 'crtr.tmux.menu.metadata',
645
+ 'crtr.tmux.menu.context',
646
+ ], entries, bindings);
647
+ entries.set('crtr.tmux.menu.show', {
648
+ description: 'show ›',
649
+ action: { kind: 'submenu', title: ' show node ', items: showItems },
650
+ });
651
+ // How this viewer PAINTS the transcript, as opposed to what it shows. Each item
652
+ // is a toggle the viewer applies to its own render state (a `/`-command sent to
653
+ // the pane); a tmux menu is built once and cannot show live state, so the item
654
+ // reads as the action and the viewer answers with a notice.
655
+ const viewSettingsItems = boundMenuItems(['crtr.tmux.menu.fold-tools'], entries, bindings);
656
+ entries.set('crtr.tmux.menu.view-settings', {
657
+ description: 'view settings ›',
658
+ action: { kind: 'submenu', title: ' view settings ', items: viewSettingsItems },
659
+ });
660
+ const issueItems = boundMenuItems([
661
+ 'crtr.tmux.menu.issue.idea',
662
+ 'crtr.tmux.menu.issue.bug',
663
+ ], entries, bindings);
664
+ entries.set('crtr.tmux.menu.issues', {
665
+ description: 'create ›',
666
+ action: { kind: 'submenu', title: ' create ', items: issueItems },
667
+ });
668
+ return entries;
669
+ }
670
+ function menuItems(bindings, entries) {
671
+ const boundItems = (ids) => boundMenuItems(ids, entries, bindings);
620
672
  const items = [];
621
673
  const addGroup = (heading, groupItems) => {
622
674
  if (groupItems.length === 0)
@@ -625,41 +677,15 @@ function menuItems(bindings) {
625
677
  items.push({ description: '', selector: '', action: { kind: 'separator' } });
626
678
  items.push({ description: `-#[bold]${heading}`, selector: '', action: { kind: 'disabled' } }, ...groupItems);
627
679
  };
628
- const showItems = boundItems([
629
- 'crtr.tmux.menu.metadata',
630
- 'crtr.tmux.menu.context',
631
- ]);
632
- // How this viewer PAINTS the transcript, as opposed to what it shows. Each item
633
- // is a toggle the viewer applies to its own render state (a `/`-command sent to
634
- // the pane); a tmux menu is built once and cannot show live state, so the item
635
- // reads as the action and the viewer answers with a notice.
636
- const viewSettingsItems = boundItems(['crtr.tmux.menu.fold-tools']);
637
- const exploreItems = boundItems([
680
+ // Explore changes what is in view or opens a canvas-level browser. Current
681
+ // node acts on the agent in this pane without changing its lifecycle.
682
+ addGroup('Explore', boundItems([
638
683
  'crtr.tmux.menu.resume',
639
684
  'crtr.tmux.menu.graph',
640
685
  'crtr.tmux.menu.focus-manager',
641
- ]);
642
- if (showItems.length > 0) {
643
- for (const gesture of bindings.gestures('crtr.tmux.menu.show')) {
644
- exploreItems.push({
645
- description: 'show ›',
646
- selector: menuSelector(gesture),
647
- action: { kind: 'submenu', title: ' show node ', items: showItems },
648
- });
649
- }
650
- }
651
- if (viewSettingsItems.length > 0) {
652
- for (const gesture of bindings.gestures('crtr.tmux.menu.view-settings')) {
653
- exploreItems.push({
654
- description: 'view settings ›',
655
- selector: menuSelector(gesture),
656
- action: { kind: 'submenu', title: ' view settings ', items: viewSettingsItems },
657
- });
658
- }
659
- }
660
- // Explore changes what is in view or opens a canvas-level browser. Current
661
- // node acts on the agent in this pane without changing its lifecycle.
662
- addGroup('Explore', exploreItems);
686
+ 'crtr.tmux.menu.show',
687
+ 'crtr.tmux.menu.view-settings',
688
+ ]));
663
689
  addGroup('Current node', boundItems([
664
690
  'crtr.tmux.menu.background-bash',
665
691
  'crtr.tmux.menu.file-review',
@@ -667,21 +693,17 @@ function menuItems(bindings) {
667
693
  'crtr.tmux.menu.copy-id',
668
694
  'crtr.tmux.menu.reload-viewer',
669
695
  ]));
670
- addGroup('Lifecycle', [
671
- ...boundItems(['crtr.tmux.menu.fork']),
672
- ...lifecycleItems,
673
- ]);
674
- const systemItems = boundItems(['crtr.tmux.menu.providers']);
675
- if (issueItems.length > 0) {
676
- for (const gesture of bindings.gestures('crtr.tmux.menu.issues')) {
677
- systemItems.push({
678
- description: 'create ›',
679
- selector: menuSelector(gesture),
680
- action: { kind: 'submenu', title: ' create ', items: issueItems },
681
- });
682
- }
683
- }
684
- addGroup('System', systemItems);
696
+ addGroup('Lifecycle', boundItems([
697
+ 'crtr.tmux.menu.fork',
698
+ 'crtr.tmux.menu.promote',
699
+ 'crtr.tmux.menu.demote',
700
+ 'crtr.tmux.menu.detach',
701
+ 'crtr.tmux.menu.close-subtree',
702
+ ]));
703
+ addGroup('System', boundItems([
704
+ 'crtr.tmux.menu.providers',
705
+ 'crtr.tmux.menu.issues',
706
+ ]));
685
707
  const prefixGestures = bindings.gestures('crtr.tmux.menu.open');
686
708
  if (prefixGestures.length > 0 && items.length > 0) {
687
709
  items.push({ description: '', selector: '', action: { kind: 'separator' } });
@@ -835,10 +857,24 @@ export function installTmuxBindings(bindings = resolveUserKeybindings()) {
835
857
  installed.push({ table, key });
836
858
  return true;
837
859
  };
838
- const menuCommand = buildMenuArgs(' crtr ', menuItems(bindings));
860
+ const menuActions = resolvedMenuActions(bindings);
861
+ const menuCommand = buildMenuArgs(' crtr ', menuItems(bindings, menuActions));
839
862
  for (const gesture of bindings.gestures('crtr.tmux.menu.open')) {
840
863
  install(ROOT_TABLE, tmuxKey(gesture), menuCommand, 'crtr.tmux.menu.open', gesture);
841
864
  }
865
+ for (const definition of BINDING_CATALOG) {
866
+ if (definition.pair?.field !== 'menu')
867
+ continue;
868
+ const menuId = definition.id;
869
+ const globalId = definition.pair.pairedId;
870
+ const entry = menuActions.get(menuId);
871
+ const command = entry === undefined ? null : menuActionCommand(entry.action);
872
+ if (command === null)
873
+ throw new Error(`no direct tmux action for paired menu binding ${menuId}`);
874
+ for (const gesture of bindings.gestures(globalId)) {
875
+ install(ROOT_TABLE, tmuxKey(gesture), command.argv, globalId, gesture);
876
+ }
877
+ }
842
878
  for (const id of ['crtr.tmux.node.next', 'crtr.tmux.node.previous', 'crtr.tmux.inbox.toggle']) {
843
879
  for (const gesture of bindings.gestures(id))
844
880
  install(ROOT_TABLE, tmuxKey(gesture), actionCommand(id), id, gesture);
@@ -87,11 +87,10 @@ export function findProjectScopeRoot(startDir = process.cwd()) {
87
87
  * nested under a directory tree that itself contains a `.crouter/` (e.g. a
88
88
  * mock-VM home under the real host home) would sweep the host's `~/.crouter`
89
89
  * (and any other above-HOME `.crouter`) in as a bogus project ancestor,
90
- * leaking one machine's user-global memory into a nested-HOME node. Mirrors
91
- * the on-read positional-walk fence (src/core/substrate/on-read.ts). A read
92
- * whose start dir is outside HOME (e.g. a production VM with HOME=/root and no
93
- * ancestor `.crouter`) never reaches home and walks to the filesystem root, as
94
- * before. */
90
+ * leaking one machine's user-global memory into a nested-HOME node. The
91
+ * explicit file-context store discovery in substrate/on-read.ts applies the
92
+ * same fence. A read whose start dir is outside HOME (e.g. a production VM
93
+ * with HOME=/root and no ancestor `.crouter`) walks to the filesystem root. */
95
94
  function collectAncestorScopeRoots(startDir, userRoot) {
96
95
  const roots = [];
97
96
  const home = realpathOrSelf(homedir());
@@ -8,13 +8,10 @@ export declare function isIndexName(name: string): boolean;
8
8
  export declare function indexDirOf(name: string): string;
9
9
  /** Returns the display path corresponding to a canonical logical identifier. */
10
10
  export declare function displayName(name: string): string;
11
- /** The two visibility surfaces an INDEX can cap. */
12
- export type Surface = 'systemPromptVisibility' | 'fileReadVisibility';
13
11
  /** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
14
12
  * callers MUST pass docs precedence-ordered (project > user > builtin) — the
15
13
  * highest-precedence INDEX governs a dir, mirroring the memory resolver. */
16
14
  export declare function buildCeilingIndex(docs: SubstrateDoc[]): Map<string, SubstrateDoc>;
17
- /** A doc's effective rung on `surface` after applying every ancestor INDEX
18
- * ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
19
- * hides the whole subtree (min with none = none). */
20
- export declare function effectiveRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>, surface: Surface): Rung;
15
+ /** A doc's effective system-prompt rung after applying every ancestor INDEX
16
+ * ceiling. A `none` ancestor hides the whole subtree from the boot catalog. */
17
+ export declare function effectiveSystemPromptRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>): Rung;
@@ -1,16 +1,15 @@
1
- // ceiling.ts — canonical INDEX identifiers and the subtree rung ceiling.
1
+ // ceiling.ts — canonical INDEX identifiers and the boot-catalog subtree ceiling.
2
2
  //
3
- // A directory index has the canonical logical identifier `<directory>/INDEX`
4
- // (`INDEX` at the root) and the same substrate frontmatter as any doc. Its rung
5
- // is a ceiling for every document in its subtree.
3
+ // A directory INDEX has the canonical logical identifier `<directory>/INDEX`
4
+ // (`INDEX` at the root). Its system-prompt rung caps every document in that
5
+ // subtree while the boot catalog renders:
6
6
  //
7
7
  // effective rung of a descendant = min(own rung, every ancestor INDEX's rung)
8
8
  //
9
- // So a directory's INDEX at `preview` lets its files surface at most at
10
- // `preview`; an INDEX at `none` hides the whole subtree. This is the single,
11
- // explicit mechanism for collapsing a directory in agent-facing surfaces (boot
12
- // render + on-read) — there is no parallel auto-hiding heuristic. See design
13
- // taste: `crtr memory read taste/document-substrate`.
9
+ // A `preview` INDEX therefore lets its descendants surface at most at preview;
10
+ // a `none` INDEX hides the subtree. File context does not use this ceiling:
11
+ // every workspace/file match is an explicit `applies-to` route and renders at
12
+ // the matched document's own file-read rung.
14
13
  import { rungRank } from './schema.js';
15
14
  /** The reserved basename (no extension) that marks a directory's index doc. */
16
15
  export const INDEX_NAME = 'INDEX';
@@ -55,15 +54,14 @@ function ancestorDirs(name) {
55
54
  out.push(parts.slice(0, i).join('/'));
56
55
  return out;
57
56
  }
58
- /** A doc's effective rung on `surface` after applying every ancestor INDEX
59
- * ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
60
- * hides the whole subtree (min with none = none). */
61
- export function effectiveRung(doc, ceil, surface) {
62
- let rung = doc[surface];
57
+ /** A doc's effective system-prompt rung after applying every ancestor INDEX
58
+ * ceiling. A `none` ancestor hides the whole subtree from the boot catalog. */
59
+ export function effectiveSystemPromptRung(doc, ceil) {
60
+ let rung = doc.systemPromptVisibility;
63
61
  for (const dir of ancestorDirs(doc.name)) {
64
62
  const idx = ceil.get(dir);
65
63
  if (idx)
66
- rung = minRung(rung, idx[surface]);
64
+ rung = minRung(rung, idx.systemPromptVisibility);
67
65
  }
68
66
  return rung;
69
67
  }
@@ -4,8 +4,7 @@ export { scopeForCwd, profileNameFor } from './subject-fields.js';
4
4
  export type { NodeConfigSubject } from './subject-fields.js';
5
5
  export { spineDepth, assembleNodeSubject } from './subject.js';
6
6
  export { gatePasses } from './gate.js';
7
- export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
8
- export type { Surface } from './ceiling.js';
7
+ export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
9
8
  export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
10
9
  export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
11
- export { renderOnReadDocs } from './on-read.js';
10
+ export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
@@ -19,7 +19,7 @@ resolveDocName, } from './schema.js';
19
19
  export { scopeForCwd, profileNameFor } from './subject-fields.js';
20
20
  export { spineDepth, assembleNodeSubject } from './subject.js';
21
21
  export { gatePasses } from './gate.js';
22
- export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
22
+ export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
23
23
  export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
24
24
  export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
25
- export { renderOnReadDocs } from './on-read.js';
25
+ export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
@@ -1,14 +1,5 @@
1
- /**
2
- * Render the substrate docs that should surface alongside a just-read file.
3
- *
4
- * @param nodeId the canvas node whose subject gates the docs.
5
- * @param readFilePath the path the `read` tool returned (absolute or not — it
6
- * is resolved to a realpath internally).
7
- * @param seen the CALLER-owned, per-session set of already-injected
8
- * doc realpaths. Docs already present are skipped; newly
9
- * injected docs are added. Pass the same set across reads
10
- * within a session (clear it on session_start) to get the
11
- * once-per-session dedup; omit it for a standalone render.
12
- * @returns the `<auto-loaded-context>` envelope, or '' when nothing surfaces.
13
- */
1
+ /** Surface docs matched by a successful read tool call. */
14
2
  export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
3
+ /** Surface workspace-wide docs during first-message assembly. `.` is a reserved
4
+ * applies-to target meaning “when this project store is mounted by cwd/profile”. */
5
+ export declare function renderWorkspaceOpenDocs(nodeId: string): string;