@north-light/crouter 0.3.221 → 0.3.222

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 (155) hide show
  1. package/dist/api/client.d.ts +21 -1
  2. package/dist/api/client.js +34 -0
  3. package/dist/api/dto/chat-inventory.d.ts +13 -0
  4. package/dist/api/dto/human-requests.d.ts +88 -0
  5. package/dist/api/dto/human-requests.js +4 -0
  6. package/dist/api/dto/human.d.ts +3 -0
  7. package/dist/api/dto/reviews.d.ts +2 -0
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -0
  10. package/dist/api/routes.d.ts +7 -0
  11. package/dist/api/routes.js +10 -0
  12. package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
  13. package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
  14. package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
  15. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
  16. package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
  17. package/dist/builtin-memory/04-base-worker.md +4 -8
  18. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  19. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
  20. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
  21. package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
  22. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
  23. package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
  24. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
  25. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
  26. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
  27. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
  28. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
  29. package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
  30. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
  31. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
  32. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
  33. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
  34. package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
  35. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
  36. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
  37. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
  38. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
  39. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
  40. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
  41. package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
  42. package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
  43. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
  44. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
  45. package/dist/builtin-memory/design/guide.md +35 -0
  46. package/dist/builtin-memory/design/roadmap.md +21 -0
  47. package/dist/builtin-memory/insights/capture.md +1 -1
  48. package/dist/builtin-memory/internal/plugins.md +10 -1
  49. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  50. package/dist/builtin-memory/plan/roadmap.md +6 -28
  51. package/dist/builtin-memory/spec/guide.md +19 -8
  52. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +28 -15
  53. package/dist/clients/attach/render/markdown-source.js +106 -1
  54. package/dist/clients/attach/session/file-links.d.ts +13 -4
  55. package/dist/clients/attach/session/file-links.js +54 -58
  56. package/dist/clients/attach/viewer.js +525 -523
  57. package/dist/clients/inbox/controller.js +1 -1
  58. package/dist/clients/inbox/resolve.d.ts +1 -0
  59. package/dist/clients/inbox/review/review-client.js +3 -1
  60. package/dist/commands/__tests__/human.test.js +2 -2
  61. package/dist/commands/human/request.d.ts +2 -0
  62. package/dist/commands/human/request.js +281 -0
  63. package/dist/commands/human.js +5 -2
  64. package/dist/commands/sys/config.js +2 -2
  65. package/dist/commands/sys/doctor.js +54 -2
  66. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  67. package/dist/core/__tests__/fixtures/memory-slash-live-probe.d.ts +1 -0
  68. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  69. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  70. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  71. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  72. package/dist/core/__tests__/human-actions.test.js +116 -0
  73. package/dist/core/__tests__/inline-memory-refs.test.js +1 -1
  74. package/dist/core/__tests__/profile-project-memory-delivery.test.js +1 -1
  75. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  76. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  77. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  78. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  79. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  80. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  81. package/dist/core/canvas/db.js +23 -0
  82. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  83. package/dist/core/canvas/human-deliveries.js +75 -0
  84. package/dist/core/config.d.ts +13 -1
  85. package/dist/core/config.js +51 -1
  86. package/dist/core/feed/inbox.d.ts +6 -0
  87. package/dist/core/feed/inbox.js +9 -1
  88. package/dist/core/human/action-binding.d.ts +21 -0
  89. package/dist/core/human/action-binding.js +40 -0
  90. package/dist/core/human/completion.d.ts +38 -0
  91. package/dist/core/human/completion.js +27 -0
  92. package/dist/core/human/convention.d.ts +2 -0
  93. package/dist/core/human/convention.js +2 -0
  94. package/dist/core/human/tickets.d.ts +25 -6
  95. package/dist/core/human/tickets.js +19 -13
  96. package/dist/core/human/types.d.ts +5 -0
  97. package/dist/core/human-actions.d.ts +25 -0
  98. package/dist/core/human-actions.js +101 -0
  99. package/dist/core/memory-resolver.js +1 -1
  100. package/dist/core/profiles/select.d.ts +2 -0
  101. package/dist/core/profiles/select.js +21 -4
  102. package/dist/core/runtime/broker/frame-dispatch.js +2 -5
  103. package/dist/core/runtime/broker-inventory.d.ts +1 -2
  104. package/dist/core/runtime/broker-inventory.js +2 -77
  105. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  106. package/dist/core/runtime/broker.js +4 -4
  107. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  108. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  109. package/dist/core/runtime/command-surface.d.ts +8 -3
  110. package/dist/core/runtime/command-surface.js +42 -6
  111. package/dist/core/runtime/launch-target.d.ts +25 -0
  112. package/dist/core/runtime/launch-target.js +54 -0
  113. package/dist/core/runtime/persona.js +3 -3
  114. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  115. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  116. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  117. package/dist/core/runtime/prospective-inventory.js +88 -0
  118. package/dist/core/runtime/spawn.d.ts +3 -1
  119. package/dist/core/runtime/spawn.js +5 -3
  120. package/dist/core/substrate/on-read.js +17 -28
  121. package/dist/core/substrate/render-node.d.ts +3 -2
  122. package/dist/core/substrate/render-node.js +3 -2
  123. package/dist/core/substrate/render.js +51 -19
  124. package/dist/core/substrate/schema.d.ts +5 -1
  125. package/dist/core/substrate/schema.js +4 -4
  126. package/dist/core/user-settings.d.ts +4 -0
  127. package/dist/core/user-settings.js +1 -0
  128. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +52 -3
  129. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  130. package/dist/daemon/api/handlers/human-requests.js +409 -0
  131. package/dist/daemon/api/handlers/human.js +3 -0
  132. package/dist/daemon/api/handlers/inbox.js +3 -0
  133. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  134. package/dist/daemon/api/handlers/nodes.js +11 -46
  135. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  136. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  137. package/dist/daemon/api/handlers/reviews.js +10 -2
  138. package/dist/daemon/api/server.js +4 -0
  139. package/dist/daemon/crtrd.js +6 -0
  140. package/dist/daemon/human/deliver-action.d.ts +16 -0
  141. package/dist/daemon/human/deliver-action.js +168 -0
  142. package/dist/daemon/human/finish.d.ts +8 -5
  143. package/dist/daemon/human/finish.js +45 -6
  144. package/dist/daemon/human/sweep.js +4 -1
  145. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  146. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  147. package/dist/daemon/review/finish.d.ts +8 -3
  148. package/dist/daemon/review/finish.js +19 -1
  149. package/dist/types.d.ts +8 -0
  150. package/dist/types.js +1 -0
  151. package/package.json +1 -1
  152. package/runtime.lock.json +2 -2
  153. package/dist/builtin-memory/00-runtime-base.md +0 -55
  154. package/dist/builtin-memory/design.md +0 -55
  155. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
@@ -0,0 +1,88 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { chatInventoryCommandRows, chatInventoryMemoryRows } from './chat-inventory-rows.js';
5
+ import { FRONT_DOOR_ENV } from './front-door-env.js';
6
+ const PREFLIGHT_TIMEOUT_MS = 15_000;
7
+ function entryPath() {
8
+ return join(dirname(fileURLToPath(import.meta.url)), 'prospective-inventory-cli.js');
9
+ }
10
+ function parseDocument(response) {
11
+ const parsed = JSON.parse(response.trim());
12
+ if (typeof parsed !== 'object' || parsed === null)
13
+ throw new Error('preflight output must be an object');
14
+ const record = parsed;
15
+ if (!Array.isArray(record['commands']) || !Array.isArray(record['memory_refs'])) {
16
+ throw new Error('preflight output must contain command and memory-ref arrays');
17
+ }
18
+ return {
19
+ commands: chatInventoryCommandRows(record['commands']),
20
+ memory_refs: chatInventoryMemoryRows(record['memory_refs']),
21
+ };
22
+ }
23
+ export function runProspectiveInventory(input) {
24
+ return new Promise((resolve, reject) => {
25
+ const env = {
26
+ ...process.env,
27
+ CRTR_PROFILE_ID: input.profileId ?? '',
28
+ [FRONT_DOOR_ENV]: '1',
29
+ };
30
+ delete env['CRTR_NODE_ID'];
31
+ const child = spawn(process.execPath, [entryPath()], {
32
+ cwd: input.cwd,
33
+ env,
34
+ stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
35
+ });
36
+ let stdout = '';
37
+ let stderr = '';
38
+ let response = '';
39
+ let settled = false;
40
+ const timer = setTimeout(() => {
41
+ if (settled)
42
+ return;
43
+ settled = true;
44
+ child.kill('SIGKILL');
45
+ reject(new Error('prospective inventory preflight timed out'));
46
+ }, PREFLIGHT_TIMEOUT_MS);
47
+ timer.unref?.();
48
+ child.stdout.setEncoding('utf8');
49
+ child.stderr.setEncoding('utf8');
50
+ const responseStream = child.stdio[3];
51
+ if (responseStream === null || responseStream === undefined || !('setEncoding' in responseStream)) {
52
+ clearTimeout(timer);
53
+ child.kill('SIGKILL');
54
+ reject(new Error('prospective inventory preflight response channel unavailable'));
55
+ return;
56
+ }
57
+ responseStream.setEncoding('utf8');
58
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
59
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
60
+ responseStream.on('data', (chunk) => { response += chunk; });
61
+ child.on('error', (error) => {
62
+ if (settled)
63
+ return;
64
+ settled = true;
65
+ clearTimeout(timer);
66
+ reject(error);
67
+ });
68
+ child.on('close', (code, signal) => {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ clearTimeout(timer);
73
+ const diagnostics = [stderr.trim(), stdout.trim()].filter(Boolean).join('\n');
74
+ if (code !== 0) {
75
+ reject(new Error(`prospective inventory preflight exited ${code ?? signal ?? 'unknown'}${diagnostics === '' ? '' : `: ${diagnostics}`}`));
76
+ return;
77
+ }
78
+ try {
79
+ resolve(parseDocument(response));
80
+ }
81
+ catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ reject(new Error(`${message}${diagnostics === '' ? '' : `: ${diagnostics}`}`));
84
+ }
85
+ });
86
+ child.stdin.end(JSON.stringify({ extensions: input.extensions }));
87
+ });
88
+ }
@@ -88,7 +88,9 @@ export declare function resolveForkSource(value: string): string;
88
88
  * shell, not from inside a node — there is no profile to inherit, so this runs
89
89
  * the same startup selector the front door uses (MRU covering `cwd`, else the
90
90
  * root-profile prompt/default). */
91
- export declare function resolveProfileId(explicit: string | null | undefined, spawner: string | null, cwd: string): Promise<string | null>;
91
+ export declare function resolveProfileId(explicit: string | null | undefined, spawner: string | null, cwd: string, options?: {
92
+ readOnlySelection?: boolean;
93
+ }): Promise<string | null>;
92
94
  /** Reject a permanently invalid launch recipe against the registry that will
93
95
  * serve it. Registered-but-unauthenticated/cooling requests intentionally pass
94
96
  * this authoring check so broker policy for transient availability is unchanged. */
@@ -13,7 +13,7 @@ import { accessSync, constants, cpSync, readdirSync, existsSync, rmSync } from '
13
13
  import { isAbsolute, resolve, join } from 'node:path';
14
14
  import { spawnNode, currentNodeContext, rootOfSpine, newNodeId, preflightNodeId } from './nodes.js';
15
15
  import { loadProfileManifest } from '../profiles/manifest.js';
16
- import { selectProfileForCwd } from '../profiles/select.js';
16
+ import { selectProfileForCwd, selectProfileForCwdReadOnly } from '../profiles/select.js';
17
17
  import { buildLaunchSpecAsync, buildPiArgv } from './launch.js';
18
18
  import { createManagedWorktree, rollbackManagedWorktree } from '../worktree.js';
19
19
  import { usage, brokerLaunchFailed } from '../errors.js';
@@ -153,7 +153,7 @@ export function resolveForkSource(value) {
153
153
  * shell, not from inside a node — there is no profile to inherit, so this runs
154
154
  * the same startup selector the front door uses (MRU covering `cwd`, else the
155
155
  * root-profile prompt/default). */
156
- export async function resolveProfileId(explicit, spawner, cwd) {
156
+ export async function resolveProfileId(explicit, spawner, cwd, options = {}) {
157
157
  if (explicit === null)
158
158
  return null;
159
159
  if (explicit !== undefined && explicit !== '') {
@@ -161,7 +161,9 @@ export async function resolveProfileId(explicit, spawner, cwd) {
161
161
  }
162
162
  if (spawner !== null)
163
163
  return getNode(spawner)?.profile_id ?? null;
164
- return selectProfileForCwd(cwd);
164
+ return options.readOnlySelection === true
165
+ ? selectProfileForCwdReadOnly(cwd)
166
+ : selectProfileForCwd(cwd);
165
167
  }
166
168
  function requestFromLaunch(launch) {
167
169
  const intent = launch.modelIntent === undefined
@@ -48,27 +48,6 @@ function attr(s) {
48
48
  .replace(/</g, '&lt;')
49
49
  .replace(/>/g, '&gt;');
50
50
  }
51
- function gitRootOf(p) {
52
- let d = p;
53
- const root = parse(d).root;
54
- while (true) {
55
- if (pathExists(`${d}${sep}.git`))
56
- return d;
57
- if (d === root)
58
- return null;
59
- const parent = dirname(d);
60
- if (parent === d)
61
- return null;
62
- d = parent;
63
- }
64
- }
65
- function disp(p) {
66
- const root = gitRootOf(p);
67
- if (!root)
68
- return p;
69
- const rel = relative(root, p);
70
- return rel === '' ? '.' : rel;
71
- }
72
51
  function isJunkAncestor(dir) {
73
52
  return dir.split(sep).some((segment) => JUNK_DIRS.has(segment));
74
53
  }
@@ -184,16 +163,26 @@ function docContentKey(doc) {
184
163
  return null;
185
164
  return `content-sha256:${createHash('sha256').update(body).digest('hex')}`;
186
165
  }
166
+ // Two shapes: element-with-body means the content is here; self-closing means
167
+ // it is not — a preview carries its routing line as `readWhen` metadata so the
168
+ // one-liner can never be mistaken for the document's content. No disk path:
169
+ // agents address memory by name through the command surface, and the explicit
170
+ // read leaf already returns `path`.
187
171
  function renderDocEnvelope(doc, rung) {
188
172
  if (rung === 'none')
189
173
  return null;
190
- let body = '';
191
- if (rung === 'content')
192
- body = doc.body.trim();
193
- else if (rung === 'preview')
194
- body = previewLine(doc);
195
- const attrs = `kind="${attr(doc.kind)}" name="${attr(doc.name)}" src="${attr(disp(doc.path))}"`;
196
- return body === '' ? `<memory ${attrs} />` : `<memory ${attrs}>\n${body}\n</memory>`;
174
+ let attrs = `kind="${attr(doc.kind)}" name="${attr(doc.name)}"`;
175
+ if (rung === 'content') {
176
+ const body = doc.body.trim();
177
+ if (body !== '')
178
+ return `<memory ${attrs}>\n${body}\n</memory>`;
179
+ }
180
+ else if (rung === 'preview') {
181
+ const pl = previewLine(doc);
182
+ if (pl !== '')
183
+ attrs += ` readWhen="${attr(pl)}"`;
184
+ }
185
+ return `<memory ${attrs} />`;
197
186
  }
198
187
  /** Render candidates at their matched rungs, gate-checked and deduped against
199
188
  * the transcript set (higher rungs pierce lower; a delivery records at its
@@ -1,6 +1,7 @@
1
1
  import type { InjectedDocs } from './injected-store.js';
2
- /** The system-prompt `<memory kind="preference">` block for `nodeId`: assemble
3
- * the node's subject from the canvas-db, render the pure block, then swap the
2
+ /** The system-prompt substrate splice for `nodeId` — shipped-layer protocol
3
+ * prose followed by the `<memory kind="preference">` block: assemble
4
+ * the node's subject from the canvas-db, render the pure splice, then swap the
4
5
  * literal `$CRTR_CONTEXT_DIR` env-var token for the node's real absolute path
5
6
  * so agents don't paste it into the non-expanding Write tool (see
6
7
  * interpolateNodePaths). A null subject (unknown id) still renders the
@@ -8,8 +8,9 @@
8
8
  import { interpolateNodePaths } from '../canvas/paths.js';
9
9
  import { assembleNodeSubject } from './subject.js';
10
10
  import { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
11
- /** The system-prompt `<memory kind="preference">` block for `nodeId`: assemble
12
- * the node's subject from the canvas-db, render the pure block, then swap the
11
+ /** The system-prompt substrate splice for `nodeId` — shipped-layer protocol
12
+ * prose followed by the `<memory kind="preference">` block: assemble
13
+ * the node's subject from the canvas-db, render the pure splice, then swap the
13
14
  * literal `$CRTR_CONTEXT_DIR` env-var token for the node's real absolute path
14
15
  * so agents don't paste it into the non-expanding Write tool (see
15
16
  * interpolateNodePaths). A null subject (unknown id) still renders the
@@ -183,9 +183,16 @@ function compareTreePosition(a, b) {
183
183
  }
184
184
  return as.length - bs.length;
185
185
  }
186
+ /** A doc the running agent can revise through `crtr memory edit`: any native
187
+ * scope-store doc (user/profile/project/node). Shipped docs — builtin and
188
+ * plugin-mounted — are not editable from a node; the preference render treats
189
+ * them as operating protocol, not memory (see buildPreferencesBlock). */
190
+ function isAgentEditable(doc) {
191
+ return doc.scope !== 'builtin' && doc.plugin === undefined;
192
+ }
186
193
  /** STEP 2 — split deduped `winners` by rung and render each group in its
187
194
  * display order: `content` docs first, as clean prose (general→specific,
188
- * then tree position, then filename — no tree chrome, no name label); every
195
+ * then tree position, then filename — no tree chrome); every
189
196
  * other winner (preview/name/hidden-bootless) folds into ONE catalog tree
190
197
  * exactly as before. `nodeLocalSet` marks which winners came from the
191
198
  * node-local store (ranked most-specific in content ordering); omit it for a
@@ -489,37 +496,62 @@ function buildSubPersonaMenu(kind) {
489
496
  lines.join('\n'));
490
497
  }
491
498
  // ---------------------------------------------------------------------------
492
- // 1. Preferences — `<memory kind="preference">` (system prompt).
499
+ // 1. Preferences — protocol prose + `<memory kind="preference">` (system prompt).
493
500
  // ---------------------------------------------------------------------------
494
- /** The system-prompt memory block: the always-present memory-usage guidance,
495
- * then (when any are eligible) the `kind: preference` docs GROUPED by rung —
496
- * content-rung docs first as clean prose (general→specific), then the
497
- * preview/name catalog tree. ALWAYS returns a non-empty `<memory
498
- * kind="preference">` block — the guidance frame is unconditional, so the
499
- * system-prompt splice is never empty even when no preference is eligible. */
501
+ /** The system-prompt substrate splice, TWO parts in one string.
502
+ *
503
+ * 1. The operating protocol: shipped (builtin/plugin) content-rung bodies as
504
+ * bare prose, plus the sub-persona menu. No memory envelope — the protocol
505
+ * is the rules of the node's world, and wrapping it as recalled "memory:
506
+ * preference" content gave it softer epistemic status than the
507
+ * instructions it is; the intro/outro sentences ("how the user wants you
508
+ * to behave", "update the preference") were also false of it.
509
+ * 2. The `<memory kind="preference">` block: the always-present memory-usage
510
+ * guidance, then (when any are eligible) each agent-editable doc wrapped
511
+ * `<preference name="…">…</preference>` — the name is the edit handle the
512
+ * outro's update directive needs — then the preview/name catalog tree.
513
+ * Every frame sentence is true of everything inside it.
514
+ *
515
+ * Ordering across the two parts is unchanged from the single-block render
516
+ * (shipped→editable was already the scope sort), so the shared cache prefix
517
+ * across nodes is preserved. ALWAYS returns a non-empty splice — the guidance
518
+ * frame is unconditional, so it is never empty even when no preference is
519
+ * eligible. */
500
520
  function buildPreferencesBlock(subject, seen) {
501
- let body = MEMORY_USAGE_GUIDANCE;
502
521
  let contentDocs = [];
503
522
  let tree = '';
523
+ let protocol = '';
524
+ let memBody = MEMORY_USAGE_GUIDANCE;
504
525
  if (subject !== null) {
505
526
  const winners = selectWinners(subject, 'preference');
506
527
  seedBootDelivery(seen, winners);
507
528
  const grouped = renderGrouped(winners, 'preferences');
508
529
  contentDocs = grouped.contentDocs;
509
530
  tree = grouped.tree;
531
+ const shipped = contentDocs
532
+ .filter((d) => !isAgentEditable(d))
533
+ .map((d) => d.body.trim())
534
+ .filter((b) => b !== '');
535
+ const editable = contentDocs
536
+ .filter((d) => isAgentEditable(d))
537
+ .map((d) => {
538
+ const b = d.body.trim();
539
+ return b === '' ? '' : `<preference name="${d.name}">\n${b}\n</preference>`;
540
+ })
541
+ .filter((b) => b !== '');
510
542
  const subPersonaMenu = buildSubPersonaMenu(subject.kind);
511
- if (grouped.contentProse !== '' || grouped.tree !== '' || subPersonaMenu !== '') {
512
- body += `\n\n${PREFERENCES_INTRO}`;
513
- if (grouped.contentProse !== '')
514
- body += `\n\n${grouped.contentProse}`;
515
- if (subPersonaMenu !== '')
516
- body += `\n\n${subPersonaMenu}`;
517
- if (grouped.tree !== '')
518
- body += `\n\n${grouped.tree}`;
519
- body += `\n\n${PREFERENCES_OUTRO}`;
543
+ protocol = [...shipped, ...(subPersonaMenu === '' ? [] : [subPersonaMenu])].join('\n\n');
544
+ if (editable.length > 0 || tree !== '') {
545
+ memBody += `\n\n${PREFERENCES_INTRO}`;
546
+ if (editable.length > 0)
547
+ memBody += `\n\n${editable.join('\n\n')}`;
548
+ if (tree !== '')
549
+ memBody += `\n\n${tree}`;
550
+ memBody += `\n\n${PREFERENCES_OUTRO}`;
520
551
  }
521
552
  }
522
- return { block: `<memory kind="preference">\n${body}\n</memory>`, contentDocs, tree };
553
+ const memoryBlock = `<memory kind="preference">\n${memBody}\n</memory>`;
554
+ return { block: protocol === '' ? memoryBlock : `${protocol}\n\n${memoryBlock}`, contentDocs, tree };
523
555
  }
524
556
  export function renderPreferencesForSubject(subject, seen) {
525
557
  return buildPreferencesBlock(subject, seen);
@@ -14,7 +14,7 @@ export declare function rungAtLeast(r: Rung, min: Rung): boolean;
14
14
  * authored rung against its project relationship's cap. */
15
15
  export declare function minRung(a: Rung, b: Rung): Rung;
16
16
  /** Strip an optional `NN-` ordering prefix from ONE path segment (file or
17
- * directory display name). `00-runtime-base` -> `runtime-base`; `spine` (no
17
+ * directory display name). `00-runtime-base/00-authoring` -> `runtime-base/authoring`; `spine` (no
18
18
  * prefix) is unchanged. */
19
19
  export declare function normalizeNameSegment(segment: string): string;
20
20
  /** Normalize a full slash-separated path-derived doc name by stripping the
@@ -111,6 +111,10 @@ export interface SubstrateDoc extends SubstrateSchema {
111
111
  name: string;
112
112
  /** The scope this doc resolved from. */
113
113
  scope: MemoryScope;
114
+ /** Owning plugin name when the doc is mounted from an installed plugin;
115
+ * undefined for a native scope doc. Shipped (builtin/plugin) docs and
116
+ * agent-editable scope docs render differently at boot (see renderGrouped). */
117
+ plugin?: string;
114
118
  /** Absolute path to the source .md. */
115
119
  path: string;
116
120
  /** Document body, frontmatter stripped. */
@@ -43,18 +43,18 @@ export function minRung(a, b) {
43
43
  // ---------------------------------------------------------------------------
44
44
  // Display-name normalization — the optional `NN-` ordering pin. A doc's
45
45
  // physical path may carry a two-digit numeric prefix on a file or directory
46
- // segment (`00-runtime-base.md`, `01-spine/`) purely to pin structural order
46
+ // segment (`00-runtime-base/`, `01-spine/`) purely to pin structural order
47
47
  // against natural sort (design: builtin prompt corpus, used sparingly). The
48
48
  // prefix is NEVER part of the doc's identity: every identity-facing surface —
49
49
  // MemoryDoc.name, direct lookup, leaf fallback, `crtr memory read`/`list`,
50
50
  // prompt render, on-read display — derives from the NORMALIZED segments, so
51
- // `00-runtime-base` displays/dedups/resolves as `runtime-base`. When a doc
51
+ // `00-runtime-base/00-authoring` displays/dedups/resolves as `runtime-base/authoring`. When a doc
52
52
  // sets an explicit frontmatter `name`, that normalized name wins instead of
53
53
  // the path-derived fallback. Only the physical path keeps the prefix.
54
54
  // ---------------------------------------------------------------------------
55
55
  const NUMERIC_PREFIX_RE = /^\d{2}-/;
56
56
  /** Strip an optional `NN-` ordering prefix from ONE path segment (file or
57
- * directory display name). `00-runtime-base` -> `runtime-base`; `spine` (no
57
+ * directory display name). `00-runtime-base/00-authoring` -> `runtime-base/authoring`; `spine` (no
58
58
  * prefix) is unchanged. */
59
59
  export function normalizeNameSegment(segment) {
60
60
  return segment.replace(NUMERIC_PREFIX_RE, '');
@@ -142,7 +142,7 @@ export function parseSubstrateDoc(doc) {
142
142
  const schema = parseSubstrateFrontmatter(doc.frontmatter);
143
143
  if (schema === null)
144
144
  return null;
145
- return { ...schema, name: doc.name, scope: doc.scope, path: doc.path, body: doc.body, projectMemory: doc.projectMemory };
145
+ return { ...schema, name: doc.name, scope: doc.scope, plugin: doc.plugin, path: doc.path, body: doc.body, projectMemory: doc.projectMemory };
146
146
  }
147
147
  /** The kind a doc resolves to for `--kind` filtering/matching: the substrate
148
148
  * kind when the frontmatter parses as a valid substrate doc, else the raw
@@ -326,6 +326,10 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
326
326
  surface: "specialized";
327
327
  owner: string;
328
328
  };
329
+ humanActions: {
330
+ surface: "specialized";
331
+ owner: string;
332
+ };
329
333
  };
330
334
  /** The exhaustive top-level key list used by config persistence and help. */
331
335
  export declare const SCOPE_CONFIG_KEYS: (keyof ScopeConfig)[];
@@ -389,6 +389,7 @@ export const CONFIG_SETTINGS_COVERAGE = {
389
389
  remoteCanvas: { surface: 'specialized', owner: 'canvas config commands' },
390
390
  spawnEnv: { surface: 'specialized', owner: 'spawn environment allow-list' },
391
391
  bin: { surface: 'specialized', owner: 'bare-binary contributions on every node PATH' },
392
+ humanActions: { surface: 'specialized', owner: 'scope-configured human completion actions' },
392
393
  };
393
394
  /** The exhaustive top-level key list used by config persistence and help. */
394
395
  export const SCOPE_CONFIG_KEYS = Object.keys(CONFIG_SETTINGS_COVERAGE);
@@ -1,16 +1,17 @@
1
1
  import { after, afterEach, beforeEach, test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { createNode, getNode, listNodes } from '../../../core/canvas/canvas.js';
7
7
  import { closeDb } from '../../../core/canvas/db.js';
8
8
  import { CrtrError } from '../../../core/errors.js';
9
- import { createProfile, loadProfileManifest, pauseProfile, setProfileDefaultKind } from '../../../core/profiles/manifest.js';
9
+ import { createProfile, loadProfileManifest, pauseProfile, profileManifestPath, ROOT_PROFILE_ID, setProfileDefaultKind } from '../../../core/profiles/manifest.js';
10
10
  import { resetScopeCache } from '../../../core/scope.js';
11
11
  import { reviveNode } from '../../../core/runtime/revive.js';
12
+ import { resolveLaunchTarget } from '../../../core/runtime/launch-target.js';
12
13
  import { spawnNode } from '../../../core/runtime/nodes.js';
13
- import { handleCreate, resolveCreateKind } from '../handlers/nodes.js';
14
+ import { handleConfig, handleCreate, resolveCreateKind } from '../handlers/nodes.js';
14
15
  const previousCrtrHome = process.env['CRTR_HOME'];
15
16
  const previousHome = process.env.HOME;
16
17
  let home = '';
@@ -101,6 +102,54 @@ test('the birth gate refuses a paused profile for a daemon-side spawn', () => {
101
102
  });
102
103
  assert.equal(listNodes().length, beforeRows);
103
104
  });
105
+ test('prospective root selection stays read-only before the root profile is scaffolded', async () => {
106
+ const manifestPath = profileManifestPath(ROOT_PROFILE_ID);
107
+ assert.equal(existsSync(manifestPath), false);
108
+ const target = await resolveLaunchTarget({ root: true, cwd }, { readOnlyProfileSelection: true });
109
+ assert.equal(target.profileId, ROOT_PROFILE_ID);
110
+ assert.equal(target.profile, null);
111
+ assert.equal(target.kind, 'general');
112
+ assert.equal(target.cwd, cwd);
113
+ assert.equal(existsSync(manifestPath), false);
114
+ });
115
+ test('target-scoped prospective validation does not rescope node writes', async () => {
116
+ mkdirSync(join(cwd, '.crouter'), { recursive: true });
117
+ writeFileSync(join(cwd, '.crouter', 'config.json'), JSON.stringify({
118
+ kinds: { 'target-only': { whenToUse: 'Only in this project.' } },
119
+ }));
120
+ const target = await resolveLaunchTarget({ root: true, cwd, kind: 'target-only' }, { readOnlyProfileSelection: true });
121
+ assert.equal(target.kind, 'target-only');
122
+ const create = {
123
+ method: 'POST',
124
+ path: '/v1/nodes',
125
+ params: {},
126
+ query: new URLSearchParams(),
127
+ body: { root: true, profile: null, cwd, kind: 'target-only' },
128
+ };
129
+ await assert.rejects(() => handleCreate(create), /unknown kind: target-only/);
130
+ const id = 'ambient-kind-write';
131
+ createNode({
132
+ node_id: id,
133
+ name: id,
134
+ created: new Date().toISOString(),
135
+ cwd,
136
+ kind: 'general',
137
+ mode: 'base',
138
+ lifecycle: 'terminal',
139
+ status: 'active',
140
+ parent: null,
141
+ profile_id: null,
142
+ });
143
+ const patch = {
144
+ method: 'PATCH',
145
+ path: `/v1/nodes/${id}/config`,
146
+ params: { id },
147
+ query: new URLSearchParams(),
148
+ body: { kind: 'target-only' },
149
+ };
150
+ await assert.rejects(() => handleConfig(patch), /unknown kind: target-only/);
151
+ assert.equal(getNode(id)?.kind, 'general');
152
+ });
104
153
  test('create kind resolves from the profile default when omitted', () => {
105
154
  const profile = createProfile('default kind', [{ path: cwd, memory: 'content' }]);
106
155
  setProfileDefaultKind(profile.profileId, 'developer');
@@ -0,0 +1,2 @@
1
+ import type { RouteTable } from '../router.js';
2
+ export declare const humanRequestRoutes: RouteTable;