@north-light/crouter 0.3.180 → 0.3.181

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 (134) hide show
  1. package/dist/api/client.d.ts +10 -1
  2. package/dist/api/client.js +13 -0
  3. package/dist/api/dto/broker.d.ts +32 -0
  4. package/dist/api/dto/crons.d.ts +17 -0
  5. package/dist/api/dto/memory.d.ts +17 -0
  6. package/dist/api/dto/memory.js +6 -0
  7. package/dist/api/dto/messages.d.ts +5 -0
  8. package/dist/api/dto/reviews.d.ts +8 -4
  9. package/dist/api/index.d.ts +1 -0
  10. package/dist/api/index.js +1 -0
  11. package/dist/api/routes.d.ts +2 -0
  12. package/dist/api/routes.js +4 -0
  13. package/dist/build-root.d.ts +7 -0
  14. package/dist/build-root.js +21 -0
  15. package/dist/builtin-memory/insights/init.md +48 -3
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/__tests__/insights-active-init.test.ts +98 -0
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +7 -50
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +16 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +34 -0
  20. package/dist/cli.js +1 -2
  21. package/dist/clients/attach/__tests__/context-message.test.js +5 -2
  22. package/dist/clients/attach/assets/README.md +7 -0
  23. package/dist/clients/attach/assets/whip-06.mp3 +0 -0
  24. package/dist/clients/attach/assets/whip-crack.mp3 +0 -0
  25. package/dist/clients/attach/assets/whip-snap.mp3 +0 -0
  26. package/dist/clients/attach/chrome/canvas-panels.d.ts +7 -1
  27. package/dist/clients/attach/chrome/canvas-panels.js +20 -3
  28. package/dist/clients/attach/chrome/review-wait.d.ts +6 -0
  29. package/dist/clients/attach/chrome/review-wait.js +22 -0
  30. package/dist/clients/attach/chrome/roster.js +23 -2
  31. package/dist/clients/attach/chrome/widgets.js +1 -1
  32. package/dist/clients/attach/input/controller.js +4 -3
  33. package/dist/clients/attach/overlays/mcp.js +3 -1
  34. package/dist/clients/attach/render/chat-view.js +1 -1
  35. package/dist/clients/attach/session/whip.d.ts +1 -0
  36. package/dist/clients/attach/session/whip.js +26 -0
  37. package/dist/clients/attach/slash/dispatch.js +2 -0
  38. package/dist/clients/attach/viewer.js +578 -573
  39. package/dist/clients/inbox/review/document-surface.d.ts +1 -1
  40. package/dist/clients/inbox/review/document-surface.js +4 -4
  41. package/dist/clients/inbox/review/launch.js +16 -4
  42. package/dist/clients/inbox/review/review-client.d.ts +9 -4
  43. package/dist/clients/inbox/review/review-client.js +3 -0
  44. package/dist/commands/cron.js +30 -8
  45. package/dist/commands/human/prompts.d.ts +7 -2
  46. package/dist/commands/human/prompts.js +15 -10
  47. package/dist/commands/human.js +1 -2
  48. package/dist/commands/memory/find.js +11 -8
  49. package/dist/commands/memory/read.js +111 -11
  50. package/dist/commands/memory/write.js +1 -1
  51. package/dist/commands/memory.js +1 -1
  52. package/dist/commands/pkg/market-manage.d.ts +13 -0
  53. package/dist/commands/pkg/market-manage.js +39 -33
  54. package/dist/commands/pkg/plugin-inspect.js +4 -3
  55. package/dist/commands/pkg/plugin-manage.js +12 -11
  56. package/dist/commands/surface/node/focus.js +1 -2
  57. package/dist/commands/sys/doctor.js +4 -4
  58. package/dist/commands/sys/setup-core.d.ts +14 -7
  59. package/dist/commands/sys/setup-core.js +66 -11
  60. package/dist/commands/sys/setup-wizard.js +2 -2
  61. package/dist/commands/sys/setup.js +1 -1
  62. package/dist/core/__tests__/cron-held-settlement.test.d.ts +1 -0
  63. package/dist/core/__tests__/cron-held-settlement.test.js +222 -0
  64. package/dist/core/__tests__/helpers/harness.js +1 -2
  65. package/dist/core/__tests__/phase4-review-store.test.js +1 -0
  66. package/dist/core/__tests__/serial/command-plugins.test.js +88 -1
  67. package/dist/core/__tests__/session-model.test.js +5 -3
  68. package/dist/core/bootstrap.d.ts +0 -4
  69. package/dist/core/bootstrap.js +1 -55
  70. package/dist/core/canvas/crons.d.ts +54 -2
  71. package/dist/core/canvas/crons.js +48 -4
  72. package/dist/core/canvas/db.js +23 -0
  73. package/dist/core/command-manifests/manifest.d.ts +11 -0
  74. package/dist/core/command-manifests/manifest.js +45 -4
  75. package/dist/core/command-manifests/schema.d.ts +1 -1
  76. package/dist/core/command-plugins/bundle.d.ts +1 -0
  77. package/dist/core/command-plugins/bundle.js +3 -3
  78. package/dist/core/command-plugins/discovery.d.ts +5 -2
  79. package/dist/core/command-plugins/discovery.js +5 -5
  80. package/dist/core/command-plugins/help-addenda.d.ts +12 -0
  81. package/dist/core/command-plugins/help-addenda.js +30 -0
  82. package/dist/core/command.js +25 -2
  83. package/dist/core/config.js +0 -1
  84. package/dist/core/human/convention.d.ts +0 -1
  85. package/dist/core/human/convention.js +0 -6
  86. package/dist/core/keybindings/inbox.d.ts +6 -8
  87. package/dist/core/keybindings/inbox.js +6 -15
  88. package/dist/core/keybindings/index.d.ts +1 -1
  89. package/dist/core/keybindings/index.js +1 -1
  90. package/dist/core/memory/doc-link-grammar.js +4 -1
  91. package/dist/core/memory-resolver.d.ts +28 -4
  92. package/dist/core/memory-resolver.js +51 -39
  93. package/dist/core/review/stage.js +1 -0
  94. package/dist/core/review/store.d.ts +5 -0
  95. package/dist/core/review/store.js +10 -0
  96. package/dist/core/review/types.d.ts +4 -0
  97. package/dist/core/runtime/broker/event-projection.d.ts +8 -1
  98. package/dist/core/runtime/broker/event-projection.js +25 -1
  99. package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
  100. package/dist/core/runtime/broker/frame-dispatch.js +50 -8
  101. package/dist/core/runtime/broker/message-ledger.d.ts +53 -0
  102. package/dist/core/runtime/broker/message-ledger.js +143 -0
  103. package/dist/core/runtime/broker/rebind.js +14 -0
  104. package/dist/core/runtime/broker-protocol.d.ts +46 -1
  105. package/dist/core/runtime/broker.js +11 -2
  106. package/dist/core/runtime/interactive-deliver.d.ts +5 -2
  107. package/dist/core/runtime/interactive-deliver.js +6 -3
  108. package/dist/core/runtime/shell-expansion.d.ts +32 -0
  109. package/dist/core/runtime/shell-expansion.js +102 -0
  110. package/dist/core/session-model/session-state.d.ts +9 -4
  111. package/dist/core/session-model/session-state.js +5 -1
  112. package/dist/daemon/api/handlers/broker-ops.js +8 -0
  113. package/dist/daemon/api/handlers/crons.js +14 -1
  114. package/dist/daemon/api/handlers/inbox.js +5 -0
  115. package/dist/daemon/api/handlers/memory.d.ts +2 -0
  116. package/dist/daemon/api/handlers/memory.js +48 -0
  117. package/dist/daemon/api/handlers/messages.js +7 -1
  118. package/dist/daemon/api/handlers/reviews.js +7 -5
  119. package/dist/daemon/api/map.js +3 -0
  120. package/dist/daemon/api/server.js +2 -0
  121. package/dist/daemon/cron-run.js +71 -3
  122. package/dist/daemon/crtrd.js +3 -0
  123. package/dist/daemon/reconcilers/pending-review-submit.d.ts +7 -0
  124. package/dist/daemon/reconcilers/pending-review-submit.js +35 -0
  125. package/dist/daemon/review/companion.d.ts +8 -0
  126. package/dist/daemon/review/companion.js +35 -0
  127. package/dist/daemon/review/deliver.js +2 -1
  128. package/dist/daemon/review/finish.d.ts +29 -2
  129. package/dist/daemon/review/finish.js +75 -2
  130. package/dist/shared/generated-context.d.ts +3 -4
  131. package/dist/shared/generated-context.js +24 -6
  132. package/dist/types.d.ts +0 -1
  133. package/package.json +1 -1
  134. package/runtime.lock.json +2 -2
@@ -37,17 +37,17 @@ export function validateCommandManifest(raw, options) {
37
37
  };
38
38
  // Top-level structure check
39
39
  if (!isRecord(raw)) {
40
- issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), options.transport === 'http' ? '{ schemaVersion, baseUrl?, timeouts?, mounts }' : '{ schemaVersion, mounts }', 'Provide a valid JSON manifest object.');
40
+ issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), options.transport === 'http' ? '{ schemaVersion, baseUrl?, timeouts?, mounts, helpAddenda? }' : '{ schemaVersion, mounts, helpAddenda? }', 'Provide a valid JSON manifest object.');
41
41
  return { issues };
42
42
  }
43
43
  // Check for unknown top-level keys
44
44
  const topKeys = Object.keys(raw);
45
45
  const allowedTopKeys = options.transport === 'http'
46
- ? new Set(['schemaVersion', 'baseUrl', 'timeouts', 'mounts'])
47
- : new Set(['schemaVersion', 'mounts']);
46
+ ? new Set(['schemaVersion', 'baseUrl', 'timeouts', 'mounts', 'helpAddenda'])
47
+ : new Set(['schemaVersion', 'mounts', 'helpAddenda']);
48
48
  const unknownKeys = topKeys.filter((k) => !allowedTopKeys.has(k));
49
49
  if (unknownKeys.length > 0) {
50
- issue('command_manifest_invalid', `unknown top-level keys`, unknownKeys.join(', '), options.transport === 'http' ? 'only: schemaVersion, baseUrl, timeouts, mounts' : 'only: schemaVersion, mounts', 'Remove the unknown keys.');
50
+ issue('command_manifest_invalid', `unknown top-level keys`, unknownKeys.join(', '), options.transport === 'http' ? 'only: schemaVersion, baseUrl, timeouts, mounts, helpAddenda' : 'only: schemaVersion, mounts, helpAddenda', 'Remove the unknown keys.');
51
51
  return { issues };
52
52
  }
53
53
  // Validate schemaVersion
@@ -83,6 +83,15 @@ export function validateCommandManifest(raw, options) {
83
83
  return { issues };
84
84
  timeouts = t;
85
85
  }
86
+ // Validate optional helpAddenda (both transports)
87
+ let helpAddenda;
88
+ if (raw['helpAddenda'] !== undefined) {
89
+ const h = validateHelpAddenda(raw['helpAddenda'], options.coreCommandPaths, issue);
90
+ if (h === null)
91
+ return { issues };
92
+ if (Object.keys(h).length > 0)
93
+ helpAddenda = h;
94
+ }
86
95
  // Validate mounts (required, non-empty)
87
96
  const mounts = raw['mounts'];
88
97
  if (!Array.isArray(mounts)) {
@@ -112,6 +121,7 @@ export function validateCommandManifest(raw, options) {
112
121
  schemaVersion: 1,
113
122
  ...(baseUrl !== undefined ? { baseUrl } : {}),
114
123
  ...(timeouts !== undefined ? { timeouts } : {}),
124
+ ...(helpAddenda !== undefined ? { helpAddenda } : {}),
115
125
  roots,
116
126
  },
117
127
  issues: [],
@@ -146,6 +156,37 @@ function validateTimeouts(raw, issue) {
146
156
  }
147
157
  return out;
148
158
  }
159
+ // ---------------------------------------------------------------------------
160
+ // Help addenda validation
161
+ // ---------------------------------------------------------------------------
162
+ /** Validate the optional top-level `helpAddenda` map: core command path →
163
+ * addendum text rendered as an attributed block beneath that core command's
164
+ * help. When the caller supplies the core command path set, a key naming no
165
+ * existing core path is rejected outright — a typo fails at the gate, never
166
+ * silently at render. */
167
+ function validateHelpAddenda(raw, coreCommandPaths, issue) {
168
+ if (!isRecord(raw)) {
169
+ issue('command_manifest_invalid', 'helpAddenda must be an object', typeName(raw), 'a map of core command path → addendum text', 'Fix helpAddenda.', 'helpAddenda');
170
+ return null;
171
+ }
172
+ const out = {};
173
+ for (const [key, value] of Object.entries(raw)) {
174
+ if (key.split(' ').some((t) => !KEBAB.test(t))) {
175
+ issue('command_help_addendum_invalid', 'helpAddenda key must be a space-separated core command path', key, 'kebab tokens separated by single spaces (e.g. "cron" or "cron add")', 'Fix the helpAddenda key.', `helpAddenda.${key}`);
176
+ return null;
177
+ }
178
+ if (typeof value !== 'string' || value.length === 0) {
179
+ issue('command_manifest_invalid', 'helpAddenda value must be a non-empty string', typeof value === 'string' ? 'empty string' : typeName(value), 'non-empty addendum text', 'Fix the helpAddenda value.', `helpAddenda.${key}`);
180
+ return null;
181
+ }
182
+ if (coreCommandPaths !== undefined && !coreCommandPaths.has(key)) {
183
+ issue('command_help_addendum_invalid', 'helpAddenda key names no crtr core command path', key, 'an existing core command path (e.g. "cron", "cron add")', 'Fix the key or remove the addendum.', `helpAddenda.${key}`);
184
+ return null;
185
+ }
186
+ out[key] = value;
187
+ }
188
+ return out;
189
+ }
149
190
  function validateMount(raw, index, transport, issue) {
150
191
  const path = `mounts[${index}]`;
151
192
  if (!isRecord(raw)) {
@@ -78,7 +78,7 @@ export interface CommandManifestIssue {
78
78
  expected: string;
79
79
  next: string;
80
80
  }
81
- export type CommandIssueCode = 'command_manifest_unreadable' | 'command_manifest_invalid' | 'command_schema_version' | 'command_path_unsafe' | 'command_not_executable' | 'command_parent_invalid' | 'command_node_invalid' | 'command_collision' | 'command_rest_invalid';
81
+ export type CommandIssueCode = 'command_manifest_unreadable' | 'command_manifest_invalid' | 'command_schema_version' | 'command_path_unsafe' | 'command_not_executable' | 'command_parent_invalid' | 'command_node_invalid' | 'command_collision' | 'command_help_addendum_invalid' | 'command_rest_invalid';
82
82
  type IssueFn = (code: CommandIssueCode, message: string, received: string, expected: string, next: string, path?: string) => void;
83
83
  export type TransportKind = 'exec' | 'http';
84
84
  export declare function validateCommandNode(raw: unknown, path: string[], topLevel: boolean, transport: TransportKind, issue: IssueFn): DeclBranch<DeclLeaf> | DeclLeaf | null;
@@ -26,4 +26,5 @@ export interface PluginBundleValidation {
26
26
  /** Validate a Core-served plugin directory archive entirely in memory. */
27
27
  export declare function validatePluginBundle(archive: Uint8Array, options: {
28
28
  reservedCoreNames: ReadonlySet<string>;
29
+ coreCommandPaths?: ReadonlySet<string>;
29
30
  }): Promise<PluginBundleValidation>;
@@ -141,7 +141,7 @@ function parseBundleMetadata(bytes) {
141
141
  }
142
142
  return undefined;
143
143
  }
144
- function parseCommands(bytes, reservedCoreNames) {
144
+ function parseCommands(bytes, reservedCoreNames, coreCommandPaths) {
145
145
  let raw;
146
146
  try {
147
147
  raw = JSON.parse(Buffer.from(bytes).toString('utf8'));
@@ -151,7 +151,7 @@ function parseCommands(bytes, reservedCoreNames) {
151
151
  issues: [bundleInvalid('commands.json is not valid JSON', 'invalid JSON', 'a valid command-manifest JSON object', 'Regenerate commands.json.', 'commands.json')],
152
152
  };
153
153
  }
154
- const validation = validateCommandManifest(raw, { transport: 'http', reservedCoreNames });
154
+ const validation = validateCommandManifest(raw, { transport: 'http', reservedCoreNames, ...(coreCommandPaths !== undefined ? { coreCommandPaths } : {}) });
155
155
  return {
156
156
  ...(validation.manifest !== undefined ? { commands: validation.manifest } : {}),
157
157
  issues: validation.issues,
@@ -178,7 +178,7 @@ export async function validatePluginBundle(archive, options) {
178
178
  const metadataIssue = parseBundleMetadata(bundle.bytes);
179
179
  if (metadataIssue !== undefined)
180
180
  return { issues: [metadataIssue] };
181
- const commandValidation = parseCommands(commands.bytes, options.reservedCoreNames);
181
+ const commandValidation = parseCommands(commands.bytes, options.reservedCoreNames, options.coreCommandPaths);
182
182
  if (commandValidation.commands === undefined)
183
183
  return { issues: commandValidation.issues };
184
184
  return {
@@ -19,13 +19,16 @@ export interface PluginCommandValidation {
19
19
  plugin: InstalledPlugin;
20
20
  manifestPath: string;
21
21
  transport?: PluginTransport;
22
+ /** The validated manifest — present only when validation fully passed.
23
+ * Carries `helpAddenda` for the help-render lookup. */
24
+ manifest?: ValidatedCommandManifest;
22
25
  contributions: ValidatedContribution[];
23
26
  issues: CommandDiscoveryIssue[];
24
27
  }
25
28
  export declare function effectiveCommandPlugins(startDir?: string, profileId?: string | null): InstalledPlugin[];
26
- export declare function validatePluginCommands(plugin: InstalledPlugin, reservedNames?: ReadonlySet<string>): PluginCommandValidation;
29
+ export declare function validatePluginCommands(plugin: InstalledPlugin, reservedNames?: ReadonlySet<string>, coreCommandPaths?: ReadonlySet<string>): PluginCommandValidation;
27
30
  export declare function discoverPluginCommandCandidates(startDir?: string, profileId?: string | null): PluginCommandValidation[];
28
- export declare function validateEffectiveCommandPlugins(reservedNames: ReadonlySet<string>, startDir?: string, profileId?: string | null): PluginCommandValidation[];
31
+ export declare function validateEffectiveCommandPlugins(reservedNames: ReadonlySet<string>, startDir?: string, profileId?: string | null, coreCommandPaths?: ReadonlySet<string>): PluginCommandValidation[];
29
32
  export declare function discoverCommandContributions(reservedNames: ReadonlySet<string>, startDir?: string, profileId?: string | null): {
30
33
  contributions: ValidatedContribution[];
31
34
  issues: CommandDiscoveryIssue[];
@@ -81,7 +81,7 @@ function validateExecExecutable(plugin, transport, manifest, issues) {
81
81
  }
82
82
  return { kind: 'exec', executable };
83
83
  }
84
- export function validatePluginCommands(plugin, reservedNames = new Set()) {
84
+ export function validatePluginCommands(plugin, reservedNames = new Set(), coreCommandPaths) {
85
85
  const issues = [];
86
86
  const commands = plugin.manifest.commands;
87
87
  const transportRaw = plugin.manifest.transport;
@@ -120,7 +120,7 @@ export function validatePluginCommands(plugin, reservedNames = new Set()) {
120
120
  issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: 'commands.json is not valid JSON', received: safe, expected: 'a JSON object', next: 'Regenerate commands.json.' });
121
121
  return { plugin, manifestPath, transport, contributions: [], issues };
122
122
  }
123
- const validation = validateCommandManifest(raw, { transport: transport.kind, reservedCoreNames: reservedNames });
123
+ const validation = validateCommandManifest(raw, { transport: transport.kind, reservedCoreNames: reservedNames, ...(coreCommandPaths !== undefined ? { coreCommandPaths } : {}) });
124
124
  if (validation.manifest === undefined) {
125
125
  validation.issues.forEach(issueFor(plugin, issues));
126
126
  return { plugin, manifestPath, transport, contributions: [], issues };
@@ -130,13 +130,13 @@ export function validatePluginCommands(plugin, reservedNames = new Set()) {
130
130
  : transport;
131
131
  if (resolvedTransport === undefined)
132
132
  return { plugin, manifestPath, transport, contributions: [], issues };
133
- return { plugin, manifestPath, transport: resolvedTransport, contributions: validation.manifest.roots.map((node) => ({ plugin, transport: resolvedTransport, node, manifest: validation.manifest })), issues };
133
+ return { plugin, manifestPath, transport: resolvedTransport, manifest: validation.manifest, contributions: validation.manifest.roots.map((node) => ({ plugin, transport: resolvedTransport, node, manifest: validation.manifest })), issues };
134
134
  }
135
135
  export function discoverPluginCommandCandidates(startDir = process.cwd(), profileId) {
136
136
  return effectiveCommandPlugins(startDir, profileId).map((plugin) => validatePluginCommands(plugin));
137
137
  }
138
- export function validateEffectiveCommandPlugins(reservedNames, startDir = process.cwd(), profileId) {
139
- const validations = effectiveCommandPlugins(startDir, profileId).map((plugin) => validatePluginCommands(plugin, reservedNames));
138
+ export function validateEffectiveCommandPlugins(reservedNames, startDir = process.cwd(), profileId, coreCommandPaths) {
139
+ const validations = effectiveCommandPlugins(startDir, profileId).map((plugin) => validatePluginCommands(plugin, reservedNames, coreCommandPaths));
140
140
  const claims = new Map();
141
141
  for (const validation of validations)
142
142
  for (const contribution of validation.contributions)
@@ -0,0 +1,12 @@
1
+ export interface HelpAddendum {
2
+ plugin: string;
3
+ text: string;
4
+ }
5
+ /** Collect every enabled plugin's addendum targeting `commandPath` (the
6
+ * space-joined walked path, e.g. "cron add"), in plugin-name order. `gate`
7
+ * carries the strict-validation inputs the install/bundle gates use; the
8
+ * help renderer already holds both (build-root exports them). */
9
+ export declare function collectHelpAddenda(commandPath: string, gate: {
10
+ reservedCoreNames: ReadonlySet<string>;
11
+ coreCommandPaths: ReadonlySet<string>;
12
+ }, startDir?: string, profileId?: string | null): HelpAddendum[];
@@ -0,0 +1,30 @@
1
+ // Plugin help addenda: a command plugin may append attributed product guidance
2
+ // beneath a CORE command's help (`helpAddenda` in its command manifest).
3
+ // Append-only, never replace — a plugin cannot alter substrate contract text.
4
+ //
5
+ // This module is the help-render lookup. It loads only on the help path
6
+ // (dynamic import from runCli) and reads stored manifest bytes only (no
7
+ // network). The caller supplies the SAME inputs the install/bundle gates pass
8
+ // to `validateCommandManifest` — the reserved core names and the full core
9
+ // command path set — so this lookup reproduces the gate's exact verdict: a
10
+ // manifest whose `helpAddenda` was rejected at ingress (reported there as a
11
+ // typed issue, e.g. `command_help_addendum_invalid`) contributes nothing at
12
+ // render either. Ingress report and render can never disagree; nothing
13
+ // disappears here that was not already loudly rejected at the gate.
14
+ import { effectiveCommandPlugins, validatePluginCommands } from './discovery.js';
15
+ /** Collect every enabled plugin's addendum targeting `commandPath` (the
16
+ * space-joined walked path, e.g. "cron add"), in plugin-name order. `gate`
17
+ * carries the strict-validation inputs the install/bundle gates use; the
18
+ * help renderer already holds both (build-root exports them). */
19
+ export function collectHelpAddenda(commandPath, gate, startDir, profileId) {
20
+ if (commandPath.length === 0)
21
+ return [];
22
+ const out = [];
23
+ for (const plugin of effectiveCommandPlugins(startDir, profileId)) {
24
+ const text = validatePluginCommands(plugin, gate.reservedCoreNames, gate.coreCommandPaths).manifest?.helpAddenda?.[commandPath];
25
+ if (text !== undefined)
26
+ out.push({ plugin: plugin.name, text });
27
+ }
28
+ out.sort((a, b) => a.plugin.localeCompare(b.plugin));
29
+ return out;
30
+ }
@@ -187,6 +187,29 @@ function renderNode(node) {
187
187
  return renderBranch(node.help);
188
188
  return renderLeafArgv(node.help);
189
189
  }
190
+ /** Render a node's help plus any plugin help addenda targeting its walked
191
+ * command path — append-only, attributed blocks a plugin adds beneath core
192
+ * contract text, never inside it. Plugin discovery loads only here (dynamic
193
+ * import on the help path), so the dispatch path's module graph and cold
194
+ * start are unchanged; the lookup reads stored manifest bytes only. The
195
+ * lookup receives the install gate's exact strict-validation inputs (reserved
196
+ * core names + the full core command path set, both from build-root), so a
197
+ * manifest rejected at ingress contributes nothing here either — loading
198
+ * every subtree for that path set is a help-path-only cost. */
199
+ async function renderNodeWithAddenda(node, path) {
200
+ const body = renderNode(node);
201
+ if (path.length === 0)
202
+ return body;
203
+ const [{ collectHelpAddenda }, { SUBTREE_NAMES, coreCommandPaths }] = await Promise.all([
204
+ import('./command-plugins/help-addenda.js'),
205
+ import('../build-root.js'),
206
+ ]);
207
+ const addenda = collectHelpAddenda(path.join(' '), {
208
+ reservedCoreNames: new Set(SUBTREE_NAMES),
209
+ coreCommandPaths: await coreCommandPaths(),
210
+ });
211
+ return body + addenda.map((a) => `\n\n<plugin-help plugin="${a.plugin}">\n${a.text}\n</plugin-help>`).join('');
212
+ }
190
213
  function helpRequested(remaining) {
191
214
  return remaining.some((t) => t === '-h' || t === '--help');
192
215
  }
@@ -599,13 +622,13 @@ export async function runCli(root, argv) {
599
622
  }
600
623
  // Help anywhere in remaining tokens → print node help and exit
601
624
  if (helpRequested(remaining)) {
602
- process.stdout.write(renderNode(node) + '\n');
625
+ process.stdout.write(await renderNodeWithAddenda(node, path) + '\n');
603
626
  process.exitCode = ExitCode.SUCCESS;
604
627
  return;
605
628
  }
606
629
  // Bare branch or bare root (no -h, remaining fully consumed) → help surface
607
630
  if (node.kind === 'root' || node.kind === 'branch') {
608
- process.stdout.write(renderNode(node) + '\n');
631
+ process.stdout.write(await renderNodeWithAddenda(node, path) + '\n');
609
632
  process.exitCode = ExitCode.SUCCESS;
610
633
  return;
611
634
  }
@@ -50,7 +50,6 @@ export function readState(scope) {
50
50
  marketplaces: existing.marketplaces ?? {},
51
51
  plugins: existing.plugins ?? {},
52
52
  last_self_check: existing.last_self_check,
53
- bootstrap_done: existing.bootstrap_done,
54
53
  activeCanvas: existing.activeCanvas ?? null,
55
54
  };
56
55
  }
@@ -10,7 +10,6 @@ export declare function isResolved(dir: string): boolean;
10
10
  export declare function isClaimed(dir: string): boolean;
11
11
  /** Resolve and verify a ticket directory without a registry. */
12
12
  export declare function requireTicket(dir: string): string;
13
- export declare function stampCanvasNode(deck: Deck): void;
14
13
  export declare function atomicWriteJson(path: string, value: unknown): void;
15
14
  /** Publish one immutable JSON record without replacing an existing winner. */
16
15
  export declare function publishJsonExclusive(path: string, value: unknown): boolean;
@@ -23,12 +23,6 @@ export function requireTicket(dir) {
23
23
  }
24
24
  return canonicalDir;
25
25
  }
26
- export function stampCanvasNode(deck) {
27
- const id = process.env['CRTR_NODE_ID'];
28
- if (id === undefined || id.trim() === '' || deck.source?.nodeId)
29
- return;
30
- deck.source = { ...(deck.source ?? {}), nodeId: id };
31
- }
32
26
  export function atomicWriteJson(path, value) {
33
27
  const tmp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
34
28
  mkdirSync(dirname(path), { recursive: true });
@@ -8,13 +8,11 @@ import type { BindingResolution } from './types.js';
8
8
  * current state; callers holding a live snapshot (e.g. the attach viewer) pass
9
9
  * it so a `/reload` rebind is honored. */
10
10
  export declare function inboxShortcut(bindings?: BindingResolution<BindingId>): string | null;
11
- /** Closed-inbox footer affordance: "<shortcut> inbox", or the CLI fallback
11
+ /** The shortcut belongs to surfaces that actually own the key — the viewer
12
+ * footer and the settings surface. Agent-facing command output never names it:
13
+ * an agent repeats the gesture back to a human who may be reading on a screen
14
+ * where it means nothing.
15
+ *
16
+ * Closed-inbox footer affordance: "<shortcut> inbox", or the CLI fallback
12
17
  * `crtr human list` when the binding is disabled. */
13
18
  export declare function inboxOpenHint(bindings?: BindingResolution<BindingId>): string;
14
- /** Bare token naming the inbox affordance for a parenthetical — the resolved
15
- * shortcut (e.g. `Alt+I`), or the backtick-quoted CLI form when disabled. */
16
- export declare function inboxHint(bindings?: BindingResolution<BindingId>): string;
17
- /** Imperative clause telling the human how to open the inbox — "press
18
- * <shortcut> in a node viewer (or run `crtr human list` for the queue)", or just
19
- * the CLI form when the binding is disabled. */
20
- export declare function inboxOpenInstruction(bindings?: BindingResolution<BindingId>): string;
@@ -12,23 +12,14 @@ export function inboxShortcut(bindings = resolveUserKeybindings()) {
12
12
  const gestures = bindings.gestures(INBOX_TOGGLE);
13
13
  return gestures.length > 0 ? gestures.map(formatGesture).join(' / ') : null;
14
14
  }
15
- /** Closed-inbox footer affordance: "<shortcut> inbox", or the CLI fallback
15
+ /** The shortcut belongs to surfaces that actually own the key — the viewer
16
+ * footer and the settings surface. Agent-facing command output never names it:
17
+ * an agent repeats the gesture back to a human who may be reading on a screen
18
+ * where it means nothing.
19
+ *
20
+ * Closed-inbox footer affordance: "<shortcut> inbox", or the CLI fallback
16
21
  * `crtr human list` when the binding is disabled. */
17
22
  export function inboxOpenHint(bindings) {
18
23
  const shortcut = inboxShortcut(bindings);
19
24
  return shortcut !== null ? `${shortcut} inbox` : 'crtr human list';
20
25
  }
21
- /** Bare token naming the inbox affordance for a parenthetical — the resolved
22
- * shortcut (e.g. `Alt+I`), or the backtick-quoted CLI form when disabled. */
23
- export function inboxHint(bindings) {
24
- return inboxShortcut(bindings) ?? '`crtr human list`';
25
- }
26
- /** Imperative clause telling the human how to open the inbox — "press
27
- * <shortcut> in a node viewer (or run `crtr human list` for the queue)", or just
28
- * the CLI form when the binding is disabled. */
29
- export function inboxOpenInstruction(bindings) {
30
- const shortcut = inboxShortcut(bindings);
31
- return shortcut !== null
32
- ? `press ${shortcut} in a node viewer (or run \`crtr human list\` for the queue)`
33
- : 'run `crtr human list` for the queue';
34
- }
@@ -1,7 +1,7 @@
1
1
  export { BINDING_CATALOG, BINDING_IDS, SAFETY_REQUIREMENTS, isAttachPaneBinding, type BindingId, } from './catalog.js';
2
2
  export { GestureSyntaxError, KeybindingValidationError, normalizeGesture, normalizeSparseOverrides, normalizeStroke, resolveKeybindings, resolveUserKeybindingSettings, resolveUserKeybindings, splitGestureExpression, type UserKeybindingSettings, } from './resolve.js';
3
3
  export { canonicalTerminalStroke, formatBinding, formatGesture, formatStroke, matchesPiTuiInput, matchesTerminalInput, type PiTuiMatcher, type RawTerminalInput, } from './match.js';
4
- export { inboxOpenHint, inboxHint, inboxOpenInstruction, inboxShortcut, } from './inbox.js';
4
+ export { inboxOpenHint, inboxShortcut, } from './inbox.js';
5
5
  export { backgroundBashHint, backgroundBashShortcut, } from './background-bash.js';
6
6
  export { ATTACH_CONTROL_BINDINGS, decodeAttachControlInput, encodeAttachControlInput, extractAttachControlInput, type AttachBindingId, type AttachMenuBindingId, type ExtractedAttachControlInput, } from './attach-control.js';
7
7
  export { KeybindingConcurrentEditError, persistUserKeybindings, type PersistedKeybindings, type PersistKeybindingsOptions, } from './persistence.js';
@@ -1,7 +1,7 @@
1
1
  export { BINDING_CATALOG, BINDING_IDS, SAFETY_REQUIREMENTS, isAttachPaneBinding, } from './catalog.js';
2
2
  export { GestureSyntaxError, KeybindingValidationError, normalizeGesture, normalizeSparseOverrides, normalizeStroke, resolveKeybindings, resolveUserKeybindingSettings, resolveUserKeybindings, splitGestureExpression, } from './resolve.js';
3
3
  export { canonicalTerminalStroke, formatBinding, formatGesture, formatStroke, matchesPiTuiInput, matchesTerminalInput, } from './match.js';
4
- export { inboxOpenHint, inboxHint, inboxOpenInstruction, inboxShortcut, } from './inbox.js';
4
+ export { inboxOpenHint, inboxShortcut, } from './inbox.js';
5
5
  export { backgroundBashHint, backgroundBashShortcut, } from './background-bash.js';
6
6
  export { ATTACH_CONTROL_BINDINGS, decodeAttachControlInput, encodeAttachControlInput, extractAttachControlInput, } from './attach-control.js';
7
7
  export { KeybindingConcurrentEditError, persistUserKeybindings, } from './persistence.js';
@@ -1,7 +1,10 @@
1
1
  // doc-link-grammar.ts — the single source of truth for the `[[canonical/name]]`
2
2
  // memory-document link grammar. Zero imports, pure string ops, browser-safe —
3
3
  // any surface that highlights, resolves, or lints doc links imports THIS module
4
- // rather than re-implementing the bracket scan or the name shape.
4
+ // rather than re-implementing the bracket scan or the name shape. It ships to
5
+ // external consumers as `@north-light/crouter-api/doc-link-grammar`, so a
6
+ // browser UI rendering a node's transcript scans with the same grammar crtrd
7
+ // validates `/v1/memory/resolve` names with.
5
8
  //
6
9
  // A doc link is a durable cross-reference written INSIDE a memory document's
7
10
  // body, pointing at another memory document by its exact canonical name (the
@@ -60,6 +60,24 @@ export interface MemoryResolutionOpts {
60
60
  }
61
61
  /** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
62
62
  export declare function memoryDocId(doc: MemoryDoc): string;
63
+ /** Whose memory view is being resolved: the workspace dir, profile, and node
64
+ * id that decide which stores exist and in what order. Ambient resolution
65
+ * builds this from the process (`process.cwd()`, `CRTR_PROFILE_ID`,
66
+ * `CRTR_NODE_ID`); a TARGET-ADDRESSED caller passes another node's, which is
67
+ * what one process serving several nodes needs — crtrd resolving a link on
68
+ * behalf of a node, or a warm-spare claim building a node's first-message
69
+ * context. The same name can name different documents for different nodes, so
70
+ * every scope in the chain (node-local, project stack, profile, user, builtin)
71
+ * reads from the target rather than the host process. */
72
+ export interface MemoryTarget {
73
+ /** The workspace dir the project-scope stack walks up from. */
74
+ cwd: string;
75
+ /** The selected profile id, or null when none. */
76
+ profileId: string | null;
77
+ /** The node whose `nodes/<id>/context/memory/` store is the nearest scope,
78
+ * or null outside a node. */
79
+ nodeId: string | null;
80
+ }
63
81
  /** All native memory docs for a scope. Project scope is a nearest-first stack of
64
82
  * every ancestor `.crouter/memory/` (widened by a selected profile's project
65
83
  * stack); profile is the selected profile's own singleton store, resolved
@@ -70,10 +88,10 @@ export declare function listMemoryDocs(scope: MemoryScope, quiet?: boolean): Mem
70
88
  * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
71
89
  * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
72
90
  export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: MemoryScope, quiet?: boolean): MemoryDoc[];
73
- /** All project-scoped docs visible from an explicit node workspace/profile.
74
- * This target-addressed form is used when one process serves several nodes
75
- * (notably warm-spare claims), where ambient cwd/profile belong to the host
76
- * process rather than the node whose first-message context is being built. */
91
+ /** All project-scoped docs visible from an explicit node workspace/profile — the
92
+ * project-only slice of a `MemoryTarget` view, used where only workspace docs
93
+ * are wanted (a workspace-open render). For a full-precedence target-addressed
94
+ * lookup, use `resolveMemoryDocForTarget`. */
77
95
  export declare function listProjectMemoryDocs(startDir?: string, profileId?: string | null, quiet?: boolean): MemoryDoc[];
78
96
  /** All memory docs across the resolved sources, in precedence order: each
79
97
  * ancestor project `.crouter/` from nearest to farthest, then the selected
@@ -93,3 +111,9 @@ export declare function createMemoryDocSnapshot(): MemoryDocSnapshot;
93
111
  * names are omitted so callers can preserve their per-document error behavior. */
94
112
  export declare function resolveMemoryDocs(names: readonly string[]): Map<string, MemoryDoc>;
95
113
  export declare function resolveMemoryDoc(rawName: string, opts?: MemoryResolutionOpts): MemoryDoc;
114
+ /** Resolve a memory document as ANOTHER node would see it — the same precedence
115
+ * chain (node-local > project stack > profile > user > builtin), read from the
116
+ * target's cwd/profile/node rather than the host process's. This is what crtrd
117
+ * resolves a `[[name]]` link through: the daemon's own cwd and env name no
118
+ * node, and the same name can be a different document for two nodes. */
119
+ export declare function resolveMemoryDocForTarget(rawName: string, target: MemoryTarget, opts?: MemoryResolutionOpts): MemoryDoc;