@north-light/crouter 0.3.163 → 0.3.165

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 (167) hide show
  1. package/dist/build-root.d.ts +6 -6
  2. package/dist/build-root.js +12 -13
  3. package/dist/builtin-memory/00-runtime-base.md +3 -4
  4. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  5. package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
  6. package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
  7. package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
  8. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
  9. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
  10. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
  11. package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
  12. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
  13. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
  14. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
  15. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
  16. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
  17. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
  18. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
  20. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
  21. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
  22. package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
  23. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
  24. package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
  25. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
  26. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
  27. package/dist/builtin-memory/advisor/council.md +40 -0
  28. package/dist/builtin-memory/design.md +8 -11
  29. package/dist/builtin-memory/development.md +6 -9
  30. package/dist/builtin-memory/internal/INDEX.md +5 -5
  31. package/dist/builtin-memory/internal/agent-shaping.md +5 -7
  32. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
  33. package/dist/builtin-memory/internal/marketplaces.md +13 -14
  34. package/dist/builtin-memory/internal/memory-loading.md +45 -0
  35. package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
  36. package/dist/builtin-memory/internal/plugins.md +61 -45
  37. package/dist/builtin-memory/planning.md +4 -7
  38. package/dist/builtin-memory/spec.md +9 -12
  39. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
  40. package/dist/cli.js +1 -1
  41. package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
  42. package/dist/clients/attach/__tests__/context-message.test.js +31 -20
  43. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
  44. package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
  45. package/dist/clients/attach/__tests__/edit-diff.test.js +21 -20
  46. package/dist/clients/attach/render/chat-view.d.ts +20 -20
  47. package/dist/clients/attach/render/chat-view.js +88 -89
  48. package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
  49. package/dist/clients/attach/render/condensed-history.js +60 -0
  50. package/dist/clients/attach/render/context-message.js +9 -14
  51. package/dist/clients/attach/render/crtr-output.d.ts +2 -1
  52. package/dist/clients/attach/render/crtr-output.js +4 -4
  53. package/dist/clients/attach/render/edit-diff.d.ts +21 -6
  54. package/dist/clients/attach/render/edit-diff.js +90 -91
  55. package/dist/clients/attach/render/tool-calls.d.ts +14 -18
  56. package/dist/clients/attach/render/tool-calls.js +66 -41
  57. package/dist/clients/attach/session/bindings.d.ts +5 -3
  58. package/dist/clients/attach/session/bindings.js +7 -13
  59. package/dist/clients/attach/session/keys.d.ts +2 -0
  60. package/dist/clients/attach/session/keys.js +36 -37
  61. package/dist/clients/attach/session/profile-files.d.ts +8 -0
  62. package/dist/clients/attach/session/profile-files.js +157 -0
  63. package/dist/clients/attach/viewer.js +530 -528
  64. package/dist/commands/memory/lint.js +1 -1
  65. package/dist/commands/memory/write.js +1 -1
  66. package/dist/commands/node.js +2 -2
  67. package/dist/commands/pkg/plugin-inspect.js +6 -7
  68. package/dist/commands/pkg/plugin-manage.d.ts +1 -1
  69. package/dist/commands/pkg/plugin-manage.js +131 -19
  70. package/dist/commands/pkg/plugin.js +2 -2
  71. package/dist/commands/pkg.js +6 -11
  72. package/dist/commands/profile/env.js +3 -3
  73. package/dist/commands/sys/config.js +17 -76
  74. package/dist/commands/sys/doctor.js +5 -91
  75. package/dist/commands/sys/setup-wizard.d.ts +9 -11
  76. package/dist/commands/sys/setup-wizard.js +47 -81
  77. package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
  78. package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
  79. package/dist/core/__tests__/command-plugins.test.js +36 -16
  80. package/dist/core/__tests__/review-model-floor.test.js +2 -2
  81. package/dist/core/__tests__/tmux-surface.test.js +10 -1
  82. package/dist/core/command-manifests/manifest.d.ts +24 -0
  83. package/dist/core/{configured-clis → command-manifests}/manifest.js +21 -25
  84. package/dist/core/command-manifests/registry.d.ts +1 -2
  85. package/dist/core/command-manifests/registry.js +2 -2
  86. package/dist/core/command-manifests/schema.d.ts +11 -13
  87. package/dist/core/command-manifests/schema.js +53 -192
  88. package/dist/core/command-plugins/compose.d.ts +0 -6
  89. package/dist/core/command-plugins/compose.js +28 -75
  90. package/dist/core/command-plugins/discovery.d.ts +25 -58
  91. package/dist/core/command-plugins/discovery.js +152 -259
  92. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  93. package/dist/core/command-plugins/endpoint.js +48 -0
  94. package/dist/core/command-plugins/store.d.ts +16 -0
  95. package/dist/core/command-plugins/store.js +64 -0
  96. package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
  97. package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
  98. package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
  99. package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
  100. package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
  101. package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
  102. package/dist/core/command.d.ts +8 -9
  103. package/dist/core/command.js +6 -9
  104. package/dist/core/config.js +6 -10
  105. package/dist/core/env-name.d.ts +6 -0
  106. package/dist/core/env-name.js +9 -0
  107. package/dist/core/io.d.ts +1 -1
  108. package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
  109. package/dist/core/keybindings/attach-control.d.ts +37 -0
  110. package/dist/core/keybindings/attach-control.js +38 -0
  111. package/dist/core/keybindings/catalog.d.ts +5 -4
  112. package/dist/core/keybindings/catalog.js +16 -8
  113. package/dist/core/keybindings/index.d.ts +1 -0
  114. package/dist/core/keybindings/index.js +1 -0
  115. package/dist/core/keybindings/types.d.ts +1 -1
  116. package/dist/core/preview-registry.js +41 -74
  117. package/dist/core/runtime/bearings.d.ts +2 -5
  118. package/dist/core/runtime/bearings.js +2 -5
  119. package/dist/core/runtime/front-door.d.ts +1 -1
  120. package/dist/core/runtime/front-door.js +2 -2
  121. package/dist/core/runtime/kickoff.d.ts +3 -3
  122. package/dist/core/runtime/kickoff.js +4 -3
  123. package/dist/core/runtime/situational-context.d.ts +1 -1
  124. package/dist/core/runtime/situational-context.js +1 -1
  125. package/dist/core/runtime/spawn.js +9 -9
  126. package/dist/core/runtime/tmux.js +29 -2
  127. package/dist/core/scope.d.ts +0 -5
  128. package/dist/core/scope.js +0 -10
  129. package/dist/core/user-settings.d.ts +193 -0
  130. package/dist/core/user-settings.js +252 -0
  131. package/dist/index.d.ts +4 -0
  132. package/dist/index.js +3 -0
  133. package/dist/pi-extensions/canvas-stophook.js +3 -2
  134. package/dist/pi-extensions/canvas-tool-guide.js +4 -1
  135. package/dist/shared/generated-context.d.ts +34 -0
  136. package/dist/shared/generated-context.js +98 -0
  137. package/dist/types.d.ts +23 -16
  138. package/dist/types.js +3 -14
  139. package/dist/web-client/assets/{index-NIuSCOHM.js → index-BMTOGuOZ.js} +19 -19
  140. package/dist/web-client/assets/index-DvA6Zw-R.css +2 -0
  141. package/dist/web-client/index.html +2 -2
  142. package/dist/web-client/sw.js +1 -1
  143. package/docs/public-api.md +1 -0
  144. package/package.json +2 -2
  145. package/runtime.lock.json +6 -6
  146. package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
  147. package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
  148. package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
  149. package/dist/builtin-memory/internal/workflow-codification.md +0 -82
  150. package/dist/builtin-memory/product.md +0 -80
  151. package/dist/clients/attach/render/frozen-history.js +0 -100
  152. package/dist/commands/pkg/cli-inspect.d.ts +0 -17
  153. package/dist/commands/pkg/cli-inspect.js +0 -190
  154. package/dist/commands/pkg/cli-manage.d.ts +0 -3
  155. package/dist/commands/pkg/cli-manage.js +0 -206
  156. package/dist/commands/pkg/cli.d.ts +0 -1
  157. package/dist/commands/pkg/cli.js +0 -14
  158. package/dist/core/configured-clis/cache.d.ts +0 -16
  159. package/dist/core/configured-clis/cache.js +0 -57
  160. package/dist/core/configured-clis/compose.d.ts +0 -14
  161. package/dist/core/configured-clis/compose.js +0 -60
  162. package/dist/core/configured-clis/discovery.d.ts +0 -47
  163. package/dist/core/configured-clis/discovery.js +0 -173
  164. package/dist/core/configured-clis/manifest.d.ts +0 -24
  165. package/dist/core/configured-clis/registration.d.ts +0 -40
  166. package/dist/core/configured-clis/registration.js +0 -201
  167. package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
@@ -13,10 +13,6 @@ import { readFault } from '../../core/runtime/fault.js';
13
13
  import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
14
14
  import { detectPackageManager } from './setup-core.js';
15
15
  import { validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
16
- import { buildExternalCommandSnapshot } from '../../core/configured-clis/discovery.js';
17
- import { effectiveConfiguredClis } from '../../core/configured-clis/registration.js';
18
- import { readConfiguredCliCacheManifest } from '../../core/configured-clis/cache.js';
19
- import { validateConfiguredCliManifest } from '../../core/configured-clis/manifest.js';
20
16
  import { SUBTREE_NAMES } from '../../build-root.js';
21
17
  import { processGeneration, processLocatedGeneration, runtimeManifest, selectedGeneration } from './shared.js';
22
18
  function pass(scope, name, message) {
@@ -132,7 +128,7 @@ function runCommandPluginChecks(scopes) {
132
128
  v.issues.forEach((issue, i) => {
133
129
  const remediation = {
134
130
  kind: 'human_action',
135
- description: `${issue.next} If unfixable, disable (pkg plugin disable ${plugin.name}), update (pkg plugin update --name ${plugin.name}), or remove (pkg plugin remove ${plugin.name}) the plugin.`,
131
+ description: `${issue.next} Inspect with \`pkg plugin show ${plugin.name}\`; then update (\`pkg plugin update --name ${plugin.name}\`) or remove (\`pkg plugin remove ${plugin.name}\`) the plugin.`,
136
132
  };
137
133
  const detail = issue.path !== undefined ? ` at ${issue.path}` : '';
138
134
  results.push(failCheck(plugin.scope, `plugin:${plugin.name}:commands[${i}]`, `${issue.code}${detail}: ${issue.message} (received ${issue.received}, expected ${issue.expected})`, remediation));
@@ -140,87 +136,6 @@ function runCommandPluginChecks(scopes) {
140
136
  }
141
137
  return results;
142
138
  }
143
- /**
144
- * Validate cached manifests and configuration of every effective configured-CLI
145
- * registration. The snapshot (E1) is used to detect cross-contributor collisions;
146
- * otherwise this check reads stored manifests only — zero network. Absent store
147
- * is a PASS (not an issue). Only registrations in the supplied scopes are checked.
148
- * --fix never fetches or removes.
149
- */
150
- function runConfiguredCliChecks(scopes) {
151
- const results = [];
152
- const inScope = new Set(scopes);
153
- const reserved = new Set(SUBTREE_NAMES);
154
- // Get collision info from the unified snapshot.
155
- const snapshot = buildExternalCommandSnapshot(reserved);
156
- const collisionsPerContributor = new Map();
157
- for (const issue of snapshot.issues) {
158
- if (issue.code === 'command_collision') {
159
- const key = `${issue.contributor.kind}:${issue.contributor.scope}:${issue.contributor.name}`;
160
- if (!collisionsPerContributor.has(key)) {
161
- collisionsPerContributor.set(key, new Set());
162
- }
163
- collisionsPerContributor.get(key).add(issue.path ?? 'unknown');
164
- }
165
- }
166
- for (const registration of effectiveConfiguredClis()) {
167
- if (!inScope.has(registration.scope))
168
- continue;
169
- // Check: store state (absent is a PASS)
170
- const cachedBytes = readConfiguredCliCacheManifest(registration);
171
- if (cachedBytes === null) {
172
- // Absent store is a PASS (not a failure); it is hydration-eligible.
173
- results.push(pass(registration.scope, `configured-cli:${registration.name}:store`, `no stored manifest (hydration-eligible on first use)`));
174
- continue;
175
- }
176
- // Have a store: check it's readable + valid JSON
177
- let raw;
178
- try {
179
- raw = JSON.parse(Buffer.from(cachedBytes).toString('utf8'));
180
- }
181
- catch {
182
- const remediation = {
183
- kind: 'human_action',
184
- description: `Stored manifest is unparseable JSON. Run \`crtr pkg cli refresh ${registration.name}\` to re-fetch, or \`crtr pkg cli remove ${registration.name}\` to drop the registration.`,
185
- command: `crtr pkg cli refresh ${registration.name}`,
186
- };
187
- results.push(failCheck(registration.scope, `configured-cli:${registration.name}:store`, `stored manifest.json is not valid JSON`, remediation));
188
- continue;
189
- }
190
- // Check: manifest §4 validation
191
- const validation = validateConfiguredCliManifest(raw, { reservedCoreNames: reserved });
192
- if (validation.issues.length > 0) {
193
- // Stored but invalid — report issues without suggesting removal (the schema itself may be fixable by a refresh).
194
- validation.issues.forEach((issue, i) => {
195
- const remediation = {
196
- kind: 'human_action',
197
- description: `${issue.next} Run \`crtr pkg cli refresh ${registration.name}\` to pull a corrected manifest, or \`crtr pkg cli remove ${registration.name}\` to drop the registration entirely.`,
198
- command: `crtr pkg cli refresh ${registration.name}`,
199
- };
200
- const detail = issue.path !== undefined ? ` at ${issue.path}` : '';
201
- results.push(failCheck(registration.scope, `configured-cli:${registration.name}:manifest[${i}]`, `${issue.code}${detail}: ${issue.message} (received ${issue.received}, expected ${issue.expected})`, remediation));
202
- });
203
- continue;
204
- }
205
- // Store valid: report success + any collisions from the snapshot
206
- const manifest = validation.manifest; // Safe: we checked validation.issues.length === 0 above
207
- const collisions = collisionsPerContributor.get(`configured-cli:${registration.scope}:${registration.name}`) ?? new Set();
208
- if (collisions.size > 0) {
209
- const paths = Array.from(collisions).sort().join(', ');
210
- const remediation = {
211
- kind: 'human_action',
212
- description: `This configured CLI's top-level command(s) or nested paths collide with core commands or other plugins. The collided paths are not mounted. Review the contributing command plugins/CLIs and resolve naming conflicts. Run \`crtr pkg cli remove ${registration.name}\` to drop this registration.`,
213
- command: `crtr pkg cli show ${registration.name}`,
214
- };
215
- results.push(failCheck(registration.scope, `configured-cli:${registration.name}:collision`, `collision(s) at: ${paths}`, remediation));
216
- }
217
- else {
218
- const topLevelNames = manifest.roots.map((root) => root.name).sort().join(', ');
219
- results.push(pass(registration.scope, `configured-cli:${registration.name}:manifest`, topLevelNames.length > 0 ? `valid manifest, contributes top-level command(s): ${topLevelNames}` : `valid manifest`));
220
- }
221
- }
222
- return results;
223
- }
224
139
  async function inventory(root) {
225
140
  const files = [];
226
141
  async function walk(dir) {
@@ -495,7 +410,7 @@ function runChecksForScope(scope, opts) {
495
410
  export const sysDoctorLeaf = defineLeaf({
496
411
  name: 'doctor',
497
412
  description: 'diagnose installation health',
498
- whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or a command-plugin manifest/executable is malformed. Reports each problem with a structured remediation, and can apply the safe repairs for you.',
413
+ whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or a plugin command manifest/transport is malformed. Reports each problem with a structured remediation, and can apply the safe repairs for you.',
499
414
  help: {
500
415
  name: 'sys doctor',
501
416
  summary: 'diagnose runtime integrity, missing manifests, and broken config entries',
@@ -505,13 +420,13 @@ export const sysDoctorLeaf = defineLeaf({
505
420
  { kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
506
421
  ],
507
422
  output: [
508
- { name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. terminal:nerd-font (user scope) reports whether a Nerd Font is installed for the viewer\'s icon glyphs; its remediation is always a human_action carrying the platform install command, never auto-fixed. Command-plugin manifest checks appear as plugin:<name>:commands (validated statically, never executed); their remediation is always a human_action (disable/update/remove the plugin). remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action; fault human_action remediations include the canonical logs command. Sorted by scope then name.' },
423
+ { name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. terminal:nerd-font (user scope) reports whether a Nerd Font is installed for the viewer\'s icon glyphs; its remediation is always a human_action carrying the platform install command, never auto-fixed. Plugin manifest checks appear as plugin:<name>:commands (validated statically, never executed); their remediation is always a human_action (disable/update/remove the plugin). remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action; fault human_action remediations include the canonical logs command. Sorted by scope then name.' },
509
424
  { name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
510
425
  ],
511
426
  outputKind: 'object',
512
427
  effects: [
513
- 'Read-only unless --fix is passed. Runtime verification reads and hashes every selected/process generation file; command-plugin manifests are validated statically — the plugin executable is never run, at any time.',
514
- 'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories; canvas fault findings and command-plugin findings stay read-only (--fix never chmods or rewrites third-party plugin content).',
428
+ 'Read-only unless --fix is passed. Runtime verification reads and hashes every selected/process generation file; plugin command manifests are validated statically — the plugin executable is never run, at any time.',
429
+ 'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories; canvas fault findings and plugin command findings stay read-only (--fix never chmods or rewrites third-party plugin content).',
515
430
  'Each non-pass result carries a structured `remediation` describing the fix action (absolute paths, exact config keys, or a human_action with its command) so callers can apply it directly without --fix.',
516
431
  ],
517
432
  },
@@ -528,7 +443,6 @@ export const sysDoctorLeaf = defineLeaf({
528
443
  if (scopes.includes('user'))
529
444
  allResults.push(runNerdFontCheck());
530
445
  allResults.push(...runCommandPluginChecks(scopes));
531
- allResults.push(...runConfiguredCliChecks(scopes));
532
446
  allResults.push(...await runFaultChecks());
533
447
  const processLocation = processGeneration();
534
448
  allResults.push(...await generationChecks('selected', selectedGeneration(), processLocation === null));
@@ -63,8 +63,8 @@ export declare class SetupWizard implements Component, Focusable {
63
63
  private keyNotice;
64
64
  private brokerLimitIndex;
65
65
  private brokerLimitNotice;
66
- private displayIndex;
67
- private displayNotice;
66
+ private settingsIndex;
67
+ private settingsNotice;
68
68
  private cachedWidth;
69
69
  private cachedHeight;
70
70
  private cachedLines;
@@ -77,14 +77,12 @@ export declare class SetupWizard implements Component, Focusable {
77
77
  invalidate(): void;
78
78
  private setTab;
79
79
  private moveTab;
80
- private moveDisplayRow;
81
- /** Advance the selected display setting to its next value and persist it to user
82
- * config immediately — these are one-keystroke view preferences, so there is no
83
- * separate save gesture to forget. A viewer picks the new value up on its next
84
- * attach (`live_cycles` / `condensed_history` / `fold_finished_tools` are all read
85
- * per ChatView), and `completion_bell` on the next turn end. */
86
- private cycleDisplaySetting;
87
- private handleDisplayInput;
80
+ private moveSettingsRow;
81
+ /** Advance the selected preference and persist it immediately to user config.
82
+ * The shared descriptor owns cycling, rendering, validation, and applicability,
83
+ * so setup and `sys config set` cannot drift onto different setting catalogs. */
84
+ private cycleSetting;
85
+ private handleSettingsInput;
88
86
  private cancel;
89
87
  dispose(): void;
90
88
  private matches;
@@ -141,7 +139,7 @@ export declare class SetupWizard implements Component, Focusable {
141
139
  private renderKeybindings;
142
140
  private renderModels;
143
141
  private renderBrokerLimits;
144
- private renderDisplay;
142
+ private renderSettings;
145
143
  render(width: number): string[];
146
144
  }
147
145
  export declare function runSetup(): Promise<void>;
@@ -10,10 +10,10 @@ import { basename, dirname, join } from 'node:path';
10
10
  import { applyTheme, defaultAgentDir } from '../../clients/attach/config.js';
11
11
  import { ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, isManagedProvider, promoteSubscription, readSubscriptionPool, renameSubscription, } from '../../core/subscription-state.js';
12
12
  import { removeProviderRow } from '../../core/provider-management.js';
13
- import { readConfig, readRawConfigAtRoot, readRawScopeConfig, updateRawConfigAtomically, persistDefaultKindModel, persistModelLadders, readMergedLaunchConfig } from '../../core/config.js';
13
+ import { readConfig, readRawConfigAtRoot, readRawScopeConfig, updateConfig, updateRawConfigAtomically, persistDefaultKindModel, persistModelLadders, readMergedLaunchConfig } from '../../core/config.js';
14
14
  import { findProjectScopeRoots, userScopeRoot } from '../../core/scope.js';
15
15
  import { loadProfileManifest, profileRoot } from '../../core/profiles/manifest.js';
16
- import { CONDENSED_HISTORY_MODES } from '../../types.js';
16
+ import { USER_SETTINGS } from '../../core/user-settings.js';
17
17
  import { BINDING_CATALOG, formatBinding, formatGesture, matchesPiTuiInput, persistUserKeybindings, resolveKeybindings, KeybindingConcurrentEditError, KeybindingValidationError, } from '../../core/keybindings/index.js';
18
18
  import { configuredLadderProviders, inferPortableModelSelection } from '../../core/runtime/model-selection.js';
19
19
  import { cliClient } from '../api-client.js';
@@ -657,45 +657,8 @@ class ProviderPanel {
657
657
  return useFrame ? framed(body, width) : body.map((line) => truncateToWidth(line, width));
658
658
  }
659
659
  }
660
- const TAB_LABELS = ['Providers', 'Plugins', 'System', 'Exa', 'Keybindings', 'Models', 'Broker limits', 'Display'];
661
- const DISPLAY_TAB = 7;
662
- /** The viewer display settings the Display tab edits, in row order. Each row owns
663
- * its own value cycling and its own formatting, so adding a setting here is the
664
- * whole change — the tab's input and render loops stay generic. Every one is a
665
- * user-scope `ScopeConfig` key persisted the moment it changes. */
666
- const DISPLAY_ROWS = [
667
- {
668
- key: 'fold_finished_tools',
669
- label: 'Fold finished tools',
670
- description: 'Fold every settled tool call to its call line (path included). Toggle live with /fold-tools.',
671
- format: (cfg) => (cfg.fold_finished_tools ? 'on' : 'off'),
672
- next: (cfg) => ({ fold_finished_tools: !cfg.fold_finished_tools }),
673
- },
674
- {
675
- key: 'completion_bell',
676
- label: 'Completion bell',
677
- description: 'Ring a short sound in the viewer when the node you are watching finishes a turn.',
678
- format: (cfg) => (cfg.completion_bell ? 'on' : 'off'),
679
- next: (cfg) => ({ completion_bell: !cfg.completion_bell }),
680
- },
681
- {
682
- key: 'live_cycles',
683
- label: 'Live cycles',
684
- description: 'Trailing refresh cycles rendered at full fidelity; older cycles condense to text. Default 2.',
685
- format: (cfg) => String(cfg.live_cycles),
686
- next: (cfg, delta) => ({ live_cycles: ((cfg.live_cycles - 1 + delta + 6) % 6) + 1 }),
687
- },
688
- {
689
- key: 'condensed_history',
690
- label: 'Condensed history',
691
- description: 'Which messages of a condensed cycle still render. Tool calls are never kept.',
692
- format: (cfg) => cfg.condensed_history,
693
- next: (cfg, delta) => ({
694
- condensed_history: CONDENSED_HISTORY_MODES[(CONDENSED_HISTORY_MODES.indexOf(cfg.condensed_history) + delta + CONDENSED_HISTORY_MODES.length)
695
- % CONDENSED_HISTORY_MODES.length],
696
- }),
697
- },
698
- ];
660
+ const TAB_LABELS = ['Providers', 'Plugins', 'System', 'Exa', 'Keybindings', 'Models', 'Broker limits', 'Settings'];
661
+ const SETTINGS_TAB = 7;
699
662
  const STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
700
663
  const LADDER_PROVIDER_KEYS = ['anthropic', 'openai'];
701
664
  const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
@@ -1304,20 +1267,21 @@ function buildSetupKeybindingRows() {
1304
1267
  }
1305
1268
  const members = [definition, paired];
1306
1269
  const menu = members.find((member) => member.pair?.field === 'menu');
1307
- const global = members.find((member) => member.pair?.field === 'global');
1308
- if (menu === undefined || global === undefined)
1270
+ const second = members.find((member) => member.pair?.field !== 'menu');
1271
+ if (menu === undefined || second === undefined)
1309
1272
  throw new Error(`incomplete keybinding pair for ${definition.id}`);
1273
+ const secondLabel = second.pair?.field === 'global' ? 'Global' : 'Direct';
1310
1274
  rows.push({
1311
1275
  group: menu.group,
1312
1276
  name: menu.name,
1313
1277
  description: menu.description,
1314
1278
  fields: [
1315
1279
  { label: 'Menu', definition: menu },
1316
- { label: 'Global', definition: global },
1280
+ { label: secondLabel, definition: second },
1317
1281
  ],
1318
1282
  });
1319
1283
  consumed.add(menu.id);
1320
- consumed.add(global.id);
1284
+ consumed.add(second.id);
1321
1285
  }
1322
1286
  return rows;
1323
1287
  }
@@ -1354,8 +1318,8 @@ export class SetupWizard {
1354
1318
  keyNotice;
1355
1319
  brokerLimitIndex = 0;
1356
1320
  brokerLimitNotice;
1357
- displayIndex = 0;
1358
- displayNotice;
1321
+ settingsIndex = 0;
1322
+ settingsNotice;
1359
1323
  cachedWidth;
1360
1324
  cachedHeight;
1361
1325
  cachedLines;
@@ -1430,33 +1394,29 @@ export class SetupWizard {
1430
1394
  const next = ((this.activeTab + delta + TAB_LABELS.length) % TAB_LABELS.length);
1431
1395
  this.setTab(next);
1432
1396
  }
1433
- moveDisplayRow(delta) {
1434
- this.displayIndex = (this.displayIndex + delta + DISPLAY_ROWS.length) % DISPLAY_ROWS.length;
1435
- this.displayNotice = undefined;
1397
+ moveSettingsRow(delta) {
1398
+ this.settingsIndex = (this.settingsIndex + delta + USER_SETTINGS.length) % USER_SETTINGS.length;
1399
+ this.settingsNotice = undefined;
1436
1400
  this.invalidate();
1437
1401
  }
1438
- /** Advance the selected display setting to its next value and persist it to user
1439
- * config immediately — these are one-keystroke view preferences, so there is no
1440
- * separate save gesture to forget. A viewer picks the new value up on its next
1441
- * attach (`live_cycles` / `condensed_history` / `fold_finished_tools` are all read
1442
- * per ChatView), and `completion_bell` on the next turn end. */
1443
- cycleDisplaySetting(delta) {
1444
- const row = DISPLAY_ROWS[this.displayIndex];
1445
- const current = readConfig('user');
1446
- const next = row.next(current, delta);
1447
- updateRawConfigAtomically('user', (latest) => ({ ...latest, ...next }));
1448
- this.displayNotice = `Saved ${row.key} = ${row.format({ ...current, ...next })}. Applies on the next attach.`;
1402
+ /** Advance the selected preference and persist it immediately to user config.
1403
+ * The shared descriptor owns cycling, rendering, validation, and applicability,
1404
+ * so setup and `sys config set` cannot drift onto different setting catalogs. */
1405
+ cycleSetting(delta) {
1406
+ const setting = USER_SETTINGS[this.settingsIndex];
1407
+ const config = updateConfig('user', (current) => setting.cycle(current, delta));
1408
+ this.settingsNotice = `Saved ${setting.key} = ${setting.format(config)}. ${setting.applies}`;
1449
1409
  this.invalidate();
1450
1410
  }
1451
- handleDisplayInput(data) {
1411
+ handleSettingsInput(data) {
1452
1412
  if (this.matches('crtr.setup.list-down', data))
1453
- this.moveDisplayRow(1);
1413
+ this.moveSettingsRow(1);
1454
1414
  else if (this.matches('crtr.setup.list-up', data))
1455
- this.moveDisplayRow(-1);
1415
+ this.moveSettingsRow(-1);
1456
1416
  else if (this.matches('crtr.setup.toggle', data) || this.matches('crtr.setup.cell-next', data))
1457
- this.cycleDisplaySetting(1);
1417
+ this.cycleSetting(1);
1458
1418
  else if (this.matches('crtr.setup.cell-previous', data))
1459
- this.cycleDisplaySetting(-1);
1419
+ this.cycleSetting(-1);
1460
1420
  else if (this.matches('crtr.setup.advance', data))
1461
1421
  this.moveTab(1);
1462
1422
  }
@@ -2049,8 +2009,8 @@ export class SetupWizard {
2049
2009
  this.invalidate();
2050
2010
  return;
2051
2011
  }
2052
- if (this.activeTab === DISPLAY_TAB) {
2053
- this.handleDisplayInput(data);
2012
+ if (this.activeTab === SETTINGS_TAB) {
2013
+ this.handleSettingsInput(data);
2054
2014
  return;
2055
2015
  }
2056
2016
  this.handleBrokerLimitsInput(data);
@@ -2077,7 +2037,7 @@ export class SetupWizard {
2077
2037
  : `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} row · ${this.label('crtr.setup.cell-previous')}/${this.label('crtr.setup.cell-next')} column · ${this.label('crtr.setup.models.scope-next')} scope · ${this.label('crtr.setup.models.open')} edit · ${this.label('crtr.setup.tab-next')} next · ${this.label('crtr.setup.cancel')} cancel`;
2078
2038
  return [truncateToWidth(theme.fg('dim', hint), width)];
2079
2039
  }
2080
- if (this.activeTab === DISPLAY_TAB) {
2040
+ if (this.activeTab === SETTINGS_TAB) {
2081
2041
  return [truncateToWidth(theme.fg('dim', `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} row · ${this.label('crtr.setup.toggle')} or ${this.label('crtr.setup.cell-next')}/${this.label('crtr.setup.cell-previous')} change value · ${tabs} · ${this.label('crtr.setup.cancel')} cancel`), width)];
2082
2042
  }
2083
2043
  if (this.activeTab === 6) {
@@ -2406,22 +2366,28 @@ export class SetupWizard {
2406
2366
  }
2407
2367
  return out;
2408
2368
  }
2409
- renderDisplay(width) {
2369
+ renderSettings(width) {
2410
2370
  const out = [];
2411
- const cfg = readConfig('user');
2412
- out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Display'))}${theme.fg('dim', ' user scope · attach viewer')}`, width));
2413
- out.push(truncateToWidth(theme.fg('dim', 'Each change saves immediately and applies to viewers attached from now on.'), width));
2371
+ const config = readConfig('user');
2372
+ const capacity = Math.max(1, Math.floor((this.terminalRows() - 13) / 2));
2373
+ const range = visibleRange(USER_SETTINGS.length, this.settingsIndex, capacity);
2374
+ out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Settings'))}${theme.fg('dim', ' user scope')}`, width));
2375
+ out.push(truncateToWidth(theme.fg('dim', 'General preferences live here; specialized settings keep their own tabs. Changes save immediately.'), width));
2414
2376
  out.push('');
2415
- for (const [index, row] of DISPLAY_ROWS.entries()) {
2416
- const selected = index === this.displayIndex;
2377
+ for (let index = range.start; index < range.end; index += 1) {
2378
+ const setting = USER_SETTINGS[index];
2379
+ const selected = index === this.settingsIndex;
2417
2380
  const marker = selected ? theme.fg('accent', '▶') : ' ';
2418
- const name = selected ? theme.fg('accent', row.label) : row.label;
2419
- out.push(truncateToWidth(`${marker} ${padAnsi(name, 24)} ${theme.fg('success', row.format(cfg))}`, width));
2420
- out.push(truncateToWidth(theme.fg('dim', ` ${row.description}`), width));
2381
+ const name = selected ? theme.fg('accent', setting.label) : setting.label;
2382
+ out.push(truncateToWidth(`${marker} ${padAnsi(name, 24)} ${theme.fg('success', setting.format(config))}`, width));
2383
+ out.push(truncateToWidth(theme.fg('dim', ` ${setting.description}`), width));
2384
+ }
2385
+ if (range.start > 0 || range.end < USER_SETTINGS.length) {
2386
+ out.push(theme.fg('dim', `(${this.settingsIndex + 1}/${USER_SETTINGS.length})`));
2421
2387
  }
2422
- if (this.displayNotice) {
2388
+ if (this.settingsNotice) {
2423
2389
  out.push('');
2424
- for (const line of wrapText(this.displayNotice, width))
2390
+ for (const line of wrapText(this.settingsNotice, width))
2425
2391
  out.push(theme.fg('success', line));
2426
2392
  }
2427
2393
  return out;
@@ -2450,7 +2416,7 @@ export class SetupWizard {
2450
2416
  ? this.renderModels(width)
2451
2417
  : this.activeTab === 6
2452
2418
  ? this.renderBrokerLimits(width)
2453
- : this.renderDisplay(width)),
2419
+ : this.renderSettings(width)),
2454
2420
  '',
2455
2421
  ...this.renderFooter(width),
2456
2422
  ];
@@ -22,39 +22,36 @@ test('base sub-personas receive hands-on guidance instead of universal delegatio
22
22
  assert.doesNotMatch(orchestrator, /You are a hands-on worker/);
23
23
  assert.match(orchestrator, /You are an orchestrator/);
24
24
  });
25
- test('developer orchestrators gate only fact-dependent decisions on explore synthesis', () => {
25
+ test('developer orchestrators receive the development front door and acceptance bar', () => {
26
26
  const prompt = renderPreferencesForSubject(subject('orchestrator', 'developer')).block;
27
- assert.match(prompt, /When a downstream decision depends on unknown source facts.*?Wait for every scout in that evidence wave's final completion result \(`crtr push final`\), not merely an update.*?synthesize the findings into one factual artifact.*?specialist who owns the decision/s);
28
- assert.match(prompt, /The scout artifact ends at evidence/);
29
- assert.match(prompt, /Independent work that does not depend on the pending facts may proceed in parallel/);
30
- assert.match(prompt, /absolute artifact path plus every relevant report or context path/);
31
- assert.doesNotMatch(prompt, /first wave explore-only|before launching downstream advisor, spec, design, plan, developer, and review children|Wait for every explore report/);
27
+ assert.match(prompt, /Before you shape a software roadmap, read `crtr memory read development` for development styles, roadmap shapes, and exit criteria that fit the goal's risk/);
28
+ assert.match(prompt, /Treat implementation as complete only when it is \*\*provably correct against the spec's acceptance criteria\*\*, not merely when it compiles/);
29
+ assert.doesNotMatch(prompt, /When a downstream decision depends on unknown source facts|Run the remaining delegation pipeline|Give each substantive implementation batch one independent review assignment/);
32
30
  });
33
- test('review guidance bounds one critique pass and closes through evidence', () => {
34
- const developer = renderPreferencesForSubject(subject('orchestrator', 'developer')).block;
35
- assert.match(developer, /one independent review assignment/);
36
- assert.match(developer, /Validation means executing acceptance criteria.*?evidence, not review/s);
37
- assert.doesNotMatch(developer, /Hand each lens to its own reviewer|idle capacity is a wasted wake|When in doubt, more rigor/);
31
+ test('review and plan orchestrators use shared review disposition rules', () => {
32
+ const sharedReviewRule = /substantive logic gets one independent review pass;.*?Its verdict is settled: fix or dismiss its findings, then validate the changed behavior rather than asking any reviewer for another opinion or PASS/s;
38
33
  const reviewOrchestrator = renderPreferencesForSubject(subject('orchestrator', 'review')).block;
39
34
  assert.match(reviewOrchestrator, /Choose the one decomposition axis/);
40
35
  assert.match(reviewOrchestrator, /at most five base review children.*?in one wave/s);
41
- assert.match(reviewOrchestrator, /Never delegate synthesis, adjudication, or validation of the synthesis/);
42
- assert.doesNotMatch(reviewOrchestrator, /Decompose the surface two ways/);
36
+ assert.match(reviewOrchestrator, sharedReviewRule);
37
+ assert.match(reviewOrchestrator, /You own synthesis and evidence reconciliation.*?The owner disposes findings and validates changed behavior/s);
38
+ assert.doesNotMatch(reviewOrchestrator, /Decompose the surface two ways|Never delegate synthesis, adjudication, or validation of the synthesis/);
43
39
  const reviewWorker = renderPreferencesForSubject(subject('base', 'review')).block;
44
40
  assert.match(reviewWorker, /A slice delegated by another reviewer remains base/);
45
41
  const planner = renderPreferencesForSubject(subject('orchestrator', 'plan')).block;
46
- assert.match(planner, /one independent review pass/);
47
- assert.doesNotMatch(planner, /Fan out your plan-reviewer sub-kinds|loops review.*?re-review/s);
42
+ assert.match(planner, sharedReviewRule);
43
+ assert.doesNotMatch(planner, /Give a consequential synthesized plan one independent review pass|requirements-coverage, pattern-consistency, code-smells, security, and architecture-fit lenses/);
48
44
  });
49
- test('explore personas keep current-state evidence separate from target-state decisions', () => {
45
+ test('explore personas preserve complete evidence without identity or tool manuals', () => {
50
46
  const base = renderPreferencesForSubject(subject('base', 'explore')).block;
51
- assert.match(base, /fast current-state codebase scout/);
47
+ assert.match(base, /Your work is \*\*read-only evidence gathering\*\* — map what exists, where it lives, how it behaves, and which constraints, gaps, or feasibility limits the source proves/);
52
48
  assert.match(base, /Root cause and recommendations belong to `advisor`, target architecture to `design`, required behavior and acceptance criteria to `spec`, and implementation decomposition to `plan`/);
53
49
  assert.match(base, /A task cannot expand your role: even when it explicitly asks, \*\*never\*\* produce those decisions/);
54
- assert.match(base, /leave any requested diagnosis, recommendation, target design, acceptance criteria, or implementation breakdown unperformed/);
55
- assert.doesNotMatch(base, /answer architecture questions|suggest changes beyond what was asked/);
50
+ assert.match(base, /Done is the \*\*requested factual surface fully mapped\*\* with evidence, not a plausible partial sketch/);
51
+ assert.match(base, /complete findings.*?exact files and line numbers.*?self-contained/s);
52
+ assert.doesNotMatch(base, /You are a fast current-state codebase scout|Use grep, find, and file reads/);
56
53
  const orchestrator = renderPreferencesForSubject(subject('orchestrator', 'explore')).block;
57
- assert.match(orchestrator, /current-state research question/);
58
- assert.match(orchestrator, /even when it explicitly asks for diagnosis or a target-state decision, gather only the facts.*?return the unperformed handoff to the matching specialist/);
54
+ assert.match(orchestrator, /even when it explicitly asks for diagnosis or a target-state decision, gather only the facts that decision needs and return the unperformed handoff to the matching specialist/);
55
+ assert.match(orchestrator, /The map is complete only when every factual sub-question is answered/);
59
56
  assert.match(orchestrator, /factual synthesis.*not a pile of transcripts or a proposed solution/);
60
57
  });
@@ -33,7 +33,6 @@ process.stdout.write(JSON.stringify({ protocolVersion: 1, ok: true, result: { ap
33
33
  function validManifest() {
34
34
  return {
35
35
  schemaVersion: 1,
36
- executable: 'bin/cmd.js',
37
36
  mounts: [
38
37
  {
39
38
  parent: [],
@@ -67,12 +66,27 @@ function validManifest() {
67
66
  };
68
67
  }
69
68
  const CASES = [
70
- (() => { const m = validManifest(); m['executable'] = '../../../../bin/sh'; return { name: 'escaping executable path', code: 'command_path_unsafe', manifest: m }; })(),
69
+ { name: 'escaping executable path', code: 'command_path_unsafe', executable: '../../../../bin/sh' },
71
70
  { name: 'non-executable file', code: 'command_not_executable', execBit: false },
72
71
  (() => { const m = validManifest(); m['schemaVersion'] = 2; return { name: 'unknown schemaVersion', code: 'command_schema_version', manifest: m }; })(),
73
72
  (() => { const m = validManifest(); m['requires'] = { crtr: '>=1' }; return { name: 'unknown top-level key', code: 'command_manifest_invalid', manifest: m }; })(),
74
- (() => { const m = validManifest(); m['mounts'][0].parent = ['sys']; return { name: 'non-empty parent', code: 'command_parent_invalid', manifest: m }; })(),
75
73
  ];
74
+ function nestedManifest() {
75
+ const manifest = validManifest();
76
+ const mounts = manifest['mounts'];
77
+ const topLevel = mounts[0];
78
+ const child = topLevel.node['children'][0];
79
+ topLevel.node['children'] = [{
80
+ kind: 'branch',
81
+ name: 'tools',
82
+ description: 'application tools',
83
+ whenToUse: 'inspect application tools',
84
+ summary: 'application tools',
85
+ children: [],
86
+ }];
87
+ mounts.push({ parent: ['app', 'tools'], node: { ...child, name: 'status' } });
88
+ return manifest;
89
+ }
76
90
  // ---------------------------------------------------------------------------
77
91
  // Isolated home + cwd
78
92
  // ---------------------------------------------------------------------------
@@ -155,7 +169,13 @@ function writePlugin(dir, name, c) {
155
169
  const root = join(dir, name);
156
170
  mkdirSync(join(root, '.crouter-plugin'), { recursive: true });
157
171
  mkdirSync(join(root, 'bin'), { recursive: true });
158
- writeFileSync(join(root, '.crouter-plugin', 'plugin.json'), JSON.stringify({ name, version: '0.1.0', description: 'fixture', commands: 'commands.json' }));
172
+ writeFileSync(join(root, '.crouter-plugin', 'plugin.json'), JSON.stringify({
173
+ name,
174
+ version: '0.1.0',
175
+ description: 'fixture',
176
+ commands: 'commands.json',
177
+ transport: { kind: 'exec', executable: c.executable ?? 'bin/cmd.js' },
178
+ }));
159
179
  writeFileSync(join(root, 'commands.json'), JSON.stringify(c.manifest ?? validManifest()));
160
180
  const execPath = join(root, 'bin', 'cmd.js');
161
181
  writeFileSync(execPath, SENTINEL_EXEC);
@@ -189,9 +209,23 @@ describe('pkg plugin show surfaces command validation', () => {
189
209
  resetScopeCache();
190
210
  const out = await pluginShow.run({ name: 'p' });
191
211
  const commands = out['commands'];
212
+ const manifest = out['manifest'];
213
+ assert.deepEqual(commands.mounts, ['app']);
214
+ assert.equal(commands.issues.length, 0);
215
+ assert.deepEqual(manifest.transport, { kind: 'exec', executable: 'bin/cmd.js' }, 'the executable belongs to plugin.json transport, not commands.json');
216
+ assertNotExecuted();
217
+ });
218
+ test('a nested mount composes into an empty exec branch instead of being rejected', async () => {
219
+ writePlugin(join(userRoot, 'plugins'), 'p', { name: 'nested mount', code: '', manifest: nestedManifest() });
220
+ resetScopeCache();
221
+ const out = await pluginShow.run({ name: 'p' });
222
+ const commands = out['commands'];
192
223
  assert.deepEqual(commands.mounts, ['app']);
193
224
  assert.equal(commands.issues.length, 0);
194
- assert.ok(typeof commands.executable === 'string', 'executable path is resolved for a valid manifest');
225
+ const snapshot = discoverCommandContributions(new Set(), emptyStart);
226
+ const tools = snapshot.contributions[0].node.children.find((node) => node.name === 'tools');
227
+ assert.equal(tools.kind, 'branch');
228
+ assert.deepEqual(tools.children.map((node) => node.name), ['status']);
195
229
  assertNotExecuted();
196
230
  });
197
231
  });