@north-light/crouter 0.3.221 → 0.3.223

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 (169) 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/memory-loading.md +4 -4
  49. package/dist/builtin-memory/internal/plugins.md +10 -1
  50. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  51. package/dist/builtin-memory/plan/roadmap.md +6 -28
  52. package/dist/builtin-memory/spec/guide.md +19 -8
  53. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +28 -15
  54. package/dist/clients/attach/render/markdown-source.js +106 -1
  55. package/dist/clients/attach/session/file-links.d.ts +13 -4
  56. package/dist/clients/attach/session/file-links.js +54 -58
  57. package/dist/clients/attach/viewer.js +545 -543
  58. package/dist/clients/inbox/controller.js +1 -1
  59. package/dist/clients/inbox/page-adapter.js +2 -1
  60. package/dist/clients/inbox/resolve.d.ts +1 -0
  61. package/dist/clients/inbox/review/review-client.js +3 -1
  62. package/dist/clients/inbox/tui/panel.js +23 -4
  63. package/dist/clients/inbox/tui/render.js +6 -0
  64. package/dist/clients/inbox/tui/types.d.ts +8 -0
  65. package/dist/commands/__tests__/human.test.js +2 -2
  66. package/dist/commands/human/request.d.ts +2 -0
  67. package/dist/commands/human/request.js +281 -0
  68. package/dist/commands/human.js +5 -2
  69. package/dist/commands/memory/shared.d.ts +2 -2
  70. package/dist/commands/memory/shared.js +11 -6
  71. package/dist/commands/pkg/browse/doc-view.js +2 -0
  72. package/dist/commands/sys/config.js +2 -2
  73. package/dist/commands/sys/doctor.js +54 -2
  74. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  75. package/dist/core/__tests__/fixtures/memory-slash-live-probe.d.ts +1 -0
  76. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  77. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  78. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  79. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  80. package/dist/core/__tests__/human-actions.test.js +116 -0
  81. package/dist/core/__tests__/inline-memory-refs.test.js +1 -1
  82. package/dist/core/__tests__/profile-project-memory-delivery.test.js +70 -3
  83. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  84. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  85. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  86. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  87. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  88. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  89. package/dist/core/canvas/db.js +23 -0
  90. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  91. package/dist/core/canvas/human-deliveries.js +75 -0
  92. package/dist/core/config.d.ts +13 -1
  93. package/dist/core/config.js +51 -1
  94. package/dist/core/feed/inbox.d.ts +6 -0
  95. package/dist/core/feed/inbox.js +9 -1
  96. package/dist/core/human/action-binding.d.ts +21 -0
  97. package/dist/core/human/action-binding.js +40 -0
  98. package/dist/core/human/completion.d.ts +38 -0
  99. package/dist/core/human/completion.js +27 -0
  100. package/dist/core/human/convention.d.ts +2 -0
  101. package/dist/core/human/convention.js +2 -0
  102. package/dist/core/human/tickets.d.ts +25 -6
  103. package/dist/core/human/tickets.js +19 -13
  104. package/dist/core/human/types.d.ts +5 -0
  105. package/dist/core/human-actions.d.ts +25 -0
  106. package/dist/core/human-actions.js +101 -0
  107. package/dist/core/memory-resolver.js +1 -1
  108. package/dist/core/profiles/select.d.ts +2 -0
  109. package/dist/core/profiles/select.js +21 -4
  110. package/dist/core/runtime/broker/frame-dispatch.js +2 -5
  111. package/dist/core/runtime/broker-inventory.d.ts +1 -2
  112. package/dist/core/runtime/broker-inventory.js +2 -77
  113. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  114. package/dist/core/runtime/broker.js +4 -4
  115. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  116. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  117. package/dist/core/runtime/command-surface.d.ts +8 -3
  118. package/dist/core/runtime/command-surface.js +42 -6
  119. package/dist/core/runtime/launch-target.d.ts +25 -0
  120. package/dist/core/runtime/launch-target.js +54 -0
  121. package/dist/core/runtime/persona.js +3 -3
  122. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  123. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  124. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  125. package/dist/core/runtime/prospective-inventory.js +88 -0
  126. package/dist/core/runtime/spawn.d.ts +3 -1
  127. package/dist/core/runtime/spawn.js +5 -3
  128. package/dist/core/substrate/frontmatter-validation.d.ts +2 -5
  129. package/dist/core/substrate/frontmatter-validation.js +8 -4
  130. package/dist/core/substrate/gate.d.ts +4 -1
  131. package/dist/core/substrate/gate.js +5 -0
  132. package/dist/core/substrate/on-read.js +21 -32
  133. package/dist/core/substrate/render-node.d.ts +3 -2
  134. package/dist/core/substrate/render-node.js +3 -2
  135. package/dist/core/substrate/render.js +64 -37
  136. package/dist/core/substrate/schema.d.ts +17 -8
  137. package/dist/core/substrate/schema.js +14 -10
  138. package/dist/core/substrate/surface-match.d.ts +8 -7
  139. package/dist/core/substrate/surface-match.js +15 -14
  140. package/dist/core/user-settings.d.ts +4 -0
  141. package/dist/core/user-settings.js +1 -0
  142. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +52 -3
  143. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  144. package/dist/daemon/api/handlers/human-requests.js +409 -0
  145. package/dist/daemon/api/handlers/human.js +3 -0
  146. package/dist/daemon/api/handlers/inbox.js +3 -0
  147. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  148. package/dist/daemon/api/handlers/nodes.js +11 -46
  149. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  150. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  151. package/dist/daemon/api/handlers/reviews.js +10 -2
  152. package/dist/daemon/api/server.js +4 -0
  153. package/dist/daemon/crtrd.js +6 -0
  154. package/dist/daemon/human/deliver-action.d.ts +16 -0
  155. package/dist/daemon/human/deliver-action.js +168 -0
  156. package/dist/daemon/human/finish.d.ts +8 -5
  157. package/dist/daemon/human/finish.js +45 -6
  158. package/dist/daemon/human/sweep.js +4 -1
  159. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  160. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  161. package/dist/daemon/review/finish.d.ts +8 -3
  162. package/dist/daemon/review/finish.js +19 -1
  163. package/dist/types.d.ts +8 -0
  164. package/dist/types.js +1 -0
  165. package/package.json +1 -1
  166. package/runtime.lock.json +2 -2
  167. package/dist/builtin-memory/00-runtime-base.md +0 -55
  168. package/dist/builtin-memory/design.md +0 -55
  169. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
@@ -0,0 +1,105 @@
1
+ import { isDeterministicCommandExpansion } from './command-expansion.js';
2
+ const MEMORY_KINDS = new Set(['knowledge', 'preference']);
3
+ const MEMORY_SCOPES = new Set(['node', 'project', 'profile', 'user', 'builtin']);
4
+ function nonEmptyString(value) {
5
+ return typeof value === 'string' && value !== '';
6
+ }
7
+ /** Keep the command rows a Gateway can use. Malformed registrations are
8
+ * discarded individually so one producer cannot take down the inventory. */
9
+ export function gatewayCommandRows(rows) {
10
+ if (!Array.isArray(rows))
11
+ return [];
12
+ const out = [];
13
+ for (const row of rows) {
14
+ if (typeof row !== 'object' || row === null)
15
+ continue;
16
+ const record = row;
17
+ if (record['gatewayCapable'] !== true)
18
+ continue;
19
+ const source = record['source'];
20
+ if (source !== 'command' && source !== 'template')
21
+ continue;
22
+ if (!nonEmptyString(record['name']) || typeof record['description'] !== 'string')
23
+ continue;
24
+ const expansion = record['expansion'];
25
+ const template = record['template'];
26
+ const argumentHint = record['argumentHint'];
27
+ out.push({
28
+ name: record['name'],
29
+ description: record['description'],
30
+ source,
31
+ ...(nonEmptyString(argumentHint) ? { argument_hint: argumentHint } : {}),
32
+ ...(isDeterministicCommandExpansion(expansion) ? { expansion } : {}),
33
+ ...(typeof template === 'string' ? { template } : {}),
34
+ });
35
+ }
36
+ return out;
37
+ }
38
+ export function chatInventoryCommandRows(rows) {
39
+ if (!Array.isArray(rows))
40
+ return [];
41
+ return rows.filter((row) => {
42
+ if (typeof row !== 'object' || row === null)
43
+ return false;
44
+ const record = row;
45
+ if (!nonEmptyString(record['name']) || typeof record['description'] !== 'string')
46
+ return false;
47
+ if (record['source'] !== 'command' && record['source'] !== 'template')
48
+ return false;
49
+ if (record['argument_hint'] !== undefined && !nonEmptyString(record['argument_hint']))
50
+ return false;
51
+ if (record['template'] !== undefined && typeof record['template'] !== 'string')
52
+ return false;
53
+ return record['expansion'] === undefined || isDeterministicCommandExpansion(record['expansion']);
54
+ });
55
+ }
56
+ export function gatewayCommandRowsFromDetail(detail) {
57
+ if (detail === undefined)
58
+ return [];
59
+ try {
60
+ return gatewayCommandRows(JSON.parse(detail));
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
66
+ export function chatInventoryMemoryRows(rows) {
67
+ if (!Array.isArray(rows))
68
+ return [];
69
+ return rows.filter((row) => {
70
+ if (typeof row !== 'object' || row === null)
71
+ return false;
72
+ const record = row;
73
+ return nonEmptyString(record['name'])
74
+ && typeof record['shortForm'] === 'string'
75
+ && typeof record['kind'] === 'string'
76
+ && MEMORY_KINDS.has(record['kind'])
77
+ && typeof record['scope'] === 'string'
78
+ && MEMORY_SCOPES.has(record['scope']);
79
+ });
80
+ }
81
+ export function gatewayMemoryRows(refs) {
82
+ if (!Array.isArray(refs))
83
+ return [];
84
+ const out = [];
85
+ for (const row of refs) {
86
+ if (typeof row !== 'object' || row === null)
87
+ continue;
88
+ const record = row;
89
+ if (record['gatewayVisible'] !== true)
90
+ continue;
91
+ if (!nonEmptyString(record['name']) || typeof record['shortForm'] !== 'string')
92
+ continue;
93
+ if (typeof record['kind'] !== 'string' || !MEMORY_KINDS.has(record['kind']))
94
+ continue;
95
+ if (typeof record['scope'] !== 'string' || !MEMORY_SCOPES.has(record['scope']))
96
+ continue;
97
+ out.push({
98
+ name: record['name'],
99
+ kind: record['kind'],
100
+ scope: record['scope'],
101
+ shortForm: record['shortForm'],
102
+ });
103
+ }
104
+ return out;
105
+ }
@@ -25,9 +25,14 @@ export declare function registrationChatSurface(command: unknown): ChatCommandSu
25
25
  * is pi's already-parsed `argument-hint`. A missing file, unreadable path, or
26
26
  * invalid YAML is not chat-capable. */
27
27
  export declare function capturePromptTemplateChatSurfaces(templates: unknown): void;
28
- /** Capture every prompt template a session currently exposes. A broken getter
29
- * leaves its templates uncaptured and therefore fail-closed. */
30
- export declare function captureSessionPromptTemplateChatSurfaces(session: unknown): void;
28
+ /** Capture the Gateway capabilities declared by one completed resource load. */
29
+ export declare function captureChatCapabilities(input: {
30
+ commands: unknown;
31
+ templates: unknown;
32
+ }): void;
33
+ /** Capture every startup-declared command and prompt template a session
34
+ * currently exposes. A broken getter leaves that surface fail-closed. */
35
+ export declare function captureSessionChatCapabilities(session: unknown): void;
31
36
  /** Read the chat surface captured for this loaded template. An uncaptured
32
37
  * template is not disclosed to chat surfaces. */
33
38
  export declare function templateChatSurface(template: unknown): ChatCommandSurface;
@@ -18,6 +18,10 @@
18
18
  import { readFileSync } from 'node:fs';
19
19
  import { parseFrontmatterGeneric } from '../frontmatter.js';
20
20
  const NOT_CAPABLE = { gatewayCapable: false };
21
+ // One resource load declares Gateway capability by extension identity and pi's
22
+ // collision-resolved invocation name. Later registrations from that extension
23
+ // retain only names the load declared.
24
+ let startupGatewayNames = new WeakMap();
21
25
  // Pi drops unknown prompt frontmatter keys. This cache restores crouter's
22
26
  // metadata without re-reading a template during command disclosure.
23
27
  const templateChatSurfaces = new WeakMap();
@@ -31,10 +35,33 @@ export function registrationChatSurface(command) {
31
35
  if (typeof command !== 'object' || command === null)
32
36
  return NOT_CAPABLE;
33
37
  const record = command;
34
- if (record['gateway'] !== true)
38
+ const { sourceInfo, invocationName } = record;
39
+ if (typeof sourceInfo !== 'object' ||
40
+ sourceInfo === null ||
41
+ typeof invocationName !== 'string' ||
42
+ !startupGatewayNames.get(sourceInfo)?.has(invocationName))
35
43
  return NOT_CAPABLE;
36
44
  return { gatewayCapable: true, ...hintOf(record['argumentHint']) };
37
45
  }
46
+ function captureExtensionCommandSurfaces(commands) {
47
+ startupGatewayNames = new WeakMap();
48
+ if (!Array.isArray(commands))
49
+ return;
50
+ for (const command of commands) {
51
+ if (typeof command !== 'object' || command === null)
52
+ continue;
53
+ const record = command;
54
+ const { sourceInfo, invocationName } = record;
55
+ if (record['gateway'] !== true ||
56
+ typeof sourceInfo !== 'object' ||
57
+ sourceInfo === null ||
58
+ typeof invocationName !== 'string')
59
+ continue;
60
+ const names = startupGatewayNames.get(sourceInfo) ?? new Set();
61
+ names.add(invocationName);
62
+ startupGatewayNames.set(sourceInfo, names);
63
+ }
64
+ }
38
65
  /** Capture prompt-template chat metadata immediately after pi loads or reloads
39
66
  * its templates. The opt-in is the template file's own frontmatter; the hint
40
67
  * is pi's already-parsed `argument-hint`. A missing file, unreadable path, or
@@ -60,16 +87,25 @@ export function capturePromptTemplateChatSurfaces(templates) {
60
87
  templateChatSurfaces.set(template, surface);
61
88
  }
62
89
  }
63
- /** Capture every prompt template a session currently exposes. A broken getter
64
- * leaves its templates uncaptured and therefore fail-closed. */
65
- export function captureSessionPromptTemplateChatSurfaces(session) {
90
+ /** Capture the Gateway capabilities declared by one completed resource load. */
91
+ export function captureChatCapabilities(input) {
92
+ captureExtensionCommandSurfaces(input.commands);
93
+ capturePromptTemplateChatSurfaces(input.templates);
94
+ }
95
+ /** Capture every startup-declared command and prompt template a session
96
+ * currently exposes. A broken getter leaves that surface fail-closed. */
97
+ export function captureSessionChatCapabilities(session) {
66
98
  try {
67
99
  if (typeof session !== 'object' || session === null)
68
100
  return;
69
- capturePromptTemplateChatSurfaces(session.promptTemplates);
101
+ const value = session;
102
+ captureChatCapabilities({
103
+ commands: value.extensionRunner?.getRegisteredCommands?.(),
104
+ templates: value.promptTemplates,
105
+ });
70
106
  }
71
107
  catch {
72
- // Fail closed when an engine cannot expose its prompt templates.
108
+ // Fail closed when an engine cannot expose its loaded resources.
73
109
  }
74
110
  }
75
111
  /** Read the chat surface captured for this loaded template. An uncaptured
@@ -0,0 +1,25 @@
1
+ import type { CreateNodeRequest } from '../../api/dto/nodes.js';
2
+ import type { Mode } from '../canvas/types.js';
3
+ import { type ProfileEntry } from '../profiles/manifest.js';
4
+ import { buildLaunchSpecAsync } from './launch.js';
5
+ export interface ResolvedLaunchTarget {
6
+ profileId: string | null;
7
+ profile: ProfileEntry | null;
8
+ kind: string;
9
+ mode: Mode;
10
+ cwd: string;
11
+ launch: Awaited<ReturnType<typeof buildLaunchSpecAsync>>['launch'];
12
+ }
13
+ export declare function resolveCreateKind(kind: string | undefined, profile: ProfileEntry | null): string;
14
+ /** Validate a kind against the same cwd/profile configuration used to build its launch.
15
+ * Resolution is subprocess-isolated: crtrd is single-threaded and a config read can
16
+ * block indefinitely on a network filesystem. */
17
+ export declare function assertLaunchKind(kind: string, cwd?: string, profileId?: string | null): Promise<void>;
18
+ /** Resolve the exact profile, kind, cwd, mode and launch recipe a create will use. */
19
+ export declare function resolveLaunchTarget(req: CreateNodeRequest, options?: {
20
+ readOnlyProfileSelection?: boolean;
21
+ /** Preserve an existing caller's validation boundary before target resolution. */
22
+ validateKind?: (kind: string) => void | Promise<void>;
23
+ }): Promise<ResolvedLaunchTarget>;
24
+ /** The directory a new node is pinned to. */
25
+ export declare function resolveNodeCwd(req: CreateNodeRequest, parent: string | undefined, profileId: string | null, contextCwd: string): string;
@@ -0,0 +1,54 @@
1
+ import { resolve } from 'node:path';
2
+ import { getNode } from '../canvas/canvas.js';
3
+ import { usage } from '../errors.js';
4
+ import { assertProfileActive, profileHome } from '../profiles/manifest.js';
5
+ import { buildLaunchSpecAsync, readMergedLaunchConfigAsync } from './launch.js';
6
+ import { resolveProfileId } from './spawn.js';
7
+ export function resolveCreateKind(kind, profile) {
8
+ return kind ?? profile?.manifest.default_kind ?? 'general';
9
+ }
10
+ /** Validate a kind against the same cwd/profile configuration used to build its launch.
11
+ * Resolution is subprocess-isolated: crtrd is single-threaded and a config read can
12
+ * block indefinitely on a network filesystem. */
13
+ export async function assertLaunchKind(kind, cwd = process.cwd(), profileId = process.env['CRTR_PROFILE_ID'] || null) {
14
+ const kinds = Object.keys((await readMergedLaunchConfigAsync(cwd, profileId)).kinds);
15
+ if (!kinds.includes(kind)) {
16
+ throw usage(`unknown kind: ${kind}`, { received: kind, next: `Valid kinds: ${kinds.sort().join(', ')}.` });
17
+ }
18
+ }
19
+ /** Resolve the exact profile, kind, cwd, mode and launch recipe a create will use. */
20
+ export async function resolveLaunchTarget(req, options = {}) {
21
+ const parent = req.parent ?? undefined;
22
+ const contextCwd = req.cwd ?? process.cwd();
23
+ const profileId = await resolveProfileId(req.profile, parent ?? null, contextCwd, {
24
+ readOnlySelection: options.readOnlyProfileSelection,
25
+ });
26
+ const profile = assertProfileActive(profileId);
27
+ const kind = resolveCreateKind(req.kind, profile);
28
+ await options.validateKind?.(kind);
29
+ const cwd = resolveNodeCwd({ ...req, kind }, parent, profileId, contextCwd);
30
+ if (options.validateKind === undefined)
31
+ await assertLaunchKind(kind, cwd, profileId);
32
+ const mode = (req.mode ?? 'base');
33
+ const launch = (await buildLaunchSpecAsync(kind, mode, {
34
+ lifecycle: req.root === true ? (req.root_lifecycle ?? 'resident') : 'terminal',
35
+ hasManager: req.root !== true,
36
+ ...(req.model !== undefined ? { model: req.model } : {}),
37
+ cwd,
38
+ profileId,
39
+ })).launch;
40
+ return { profileId, profile, kind, mode, cwd, launch };
41
+ }
42
+ /** The directory a new node is pinned to. */
43
+ export function resolveNodeCwd(req, parent, profileId, contextCwd) {
44
+ if (req.pin_cwd !== undefined && req.pin_cwd !== '')
45
+ return resolve(req.pin_cwd);
46
+ if (parent != null) {
47
+ const spawner = getNode(parent);
48
+ if (spawner !== null)
49
+ return spawner.cwd;
50
+ }
51
+ if (req.cwd !== undefined && req.cwd !== '')
52
+ return resolve(req.cwd);
53
+ return profileHome(profileId) ?? contextCwd;
54
+ }
@@ -79,15 +79,15 @@ function baseModeGuidance() {
79
79
  }
80
80
  // The lifecycle transition prose is the SAME contract that's baked into the
81
81
  // static system prompt at birth — so both load from the one source
82
- // (`personas/lifecycle/{terminal,resident}.md`) and can never drift. The flip
82
+ // (`turn-lifecycle/{terminal,resident}.md`) and can never drift. The flip
83
83
  // only re-delivers that fragment as the node's new-state steer.
84
84
  /** terminal → resident: interactable, never forced to submit. */
85
85
  function residentLifecycleGuidance() {
86
- return loadMemoryBody('lifecycle/resident');
86
+ return loadMemoryBody('turn-lifecycle/resident');
87
87
  }
88
88
  /** resident → terminal: owes a final, reaps when done. */
89
89
  function terminalLifecycleGuidance() {
90
- return loadMemoryBody('lifecycle/terminal');
90
+ return loadMemoryBody('turn-lifecycle/terminal');
91
91
  }
92
92
  /** Build the injected transition prompt for a `from → to` persona change.
93
93
  * Concatenates the relevant section per changed axis (both when both changed).
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { DefaultResourceLoader, ExtensionRunner, getAgentDir, } from '@earendil-works/pi-coding-agent';
3
+ import { buildRefInventory } from '../memory/inline-ref-inventory.js';
4
+ import { gatewayCommandRows, gatewayMemoryRows } from './chat-inventory-rows.js';
5
+ import { captureChatCapabilities, registrationChatSurface, templateChatSurface, } from './command-surface.js';
6
+ function parseInput() {
7
+ const parsed = JSON.parse(readFileSync(0, 'utf8'));
8
+ if (typeof parsed !== 'object' || parsed === null)
9
+ throw new Error('preflight input must be an object');
10
+ const extensions = parsed.extensions;
11
+ if (!Array.isArray(extensions) || !extensions.every((value) => typeof value === 'string')) {
12
+ throw new Error('preflight input extensions must be strings');
13
+ }
14
+ return { extensions };
15
+ }
16
+ async function main() {
17
+ const { extensions } = parseInput();
18
+ const cwd = process.cwd();
19
+ const loader = new DefaultResourceLoader({
20
+ cwd,
21
+ agentDir: getAgentDir(),
22
+ additionalExtensionPaths: extensions,
23
+ noContextFiles: true,
24
+ });
25
+ await loader.reload();
26
+ const loaded = loader.getExtensions();
27
+ const templates = loader.getPrompts().prompts;
28
+ // getRegisteredCommands only reads extension maps and applies pi's collision
29
+ // naming. The unbound placeholders make every action path unavailable.
30
+ const runner = new ExtensionRunner(loaded.extensions, loaded.runtime, cwd, undefined, undefined);
31
+ const commands = runner.getRegisteredCommands();
32
+ captureChatCapabilities({ commands, templates });
33
+ const disclosures = [
34
+ ...commands.map((command) => {
35
+ const expansion = command.expansion;
36
+ return {
37
+ name: command.invocationName,
38
+ description: command.description ?? '',
39
+ source: 'command',
40
+ ...(expansion !== undefined ? { expansion } : {}),
41
+ ...registrationChatSurface(command),
42
+ };
43
+ }),
44
+ ...templates.map((template) => ({
45
+ name: template.name,
46
+ description: template.description,
47
+ source: 'template',
48
+ template: template.content,
49
+ ...templateChatSurface(template),
50
+ })),
51
+ ];
52
+ const memory = buildRefInventory();
53
+ writeFileSync(3, `${JSON.stringify({
54
+ commands: gatewayCommandRows(disclosures),
55
+ memory_refs: gatewayMemoryRows(memory.refs),
56
+ })}\n`);
57
+ }
58
+ main().catch((error) => {
59
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
60
+ process.exitCode = 1;
61
+ });
@@ -0,0 +1,10 @@
1
+ import type { ChatInventoryCommandDTO, ChatInventoryMemoryRefDTO } from '../../api/dto/chat-inventory.js';
2
+ export interface ProspectiveInventoryRows {
3
+ commands: ChatInventoryCommandDTO[];
4
+ memory_refs: ChatInventoryMemoryRefDTO[];
5
+ }
6
+ export declare function runProspectiveInventory(input: {
7
+ cwd: string;
8
+ profileId: string | null;
9
+ extensions: string[];
10
+ }): Promise<ProspectiveInventoryRows>;
@@ -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
@@ -1,3 +1,4 @@
1
+ import { type SurfaceEntry } from './schema.js';
1
2
  /** Strict validation of one raw routing entry. Runtime parsing deliberately
2
3
  * tolerates malformed docs; this authoring/preflight contract does not. */
3
4
  export declare function lintSubstrateSurfaces(value: unknown): string | null;
@@ -6,8 +7,4 @@ export declare function lintSubstrateFrontmatter(fm: Record<string, unknown> | n
6
7
  /** Compatibility name for consumers that adopted lint's former export. */
7
8
  export declare const lintSubstrateSchema: typeof lintSubstrateFrontmatter;
8
9
  /** Parsed routing entries for checks that run after strict validation. */
9
- export declare function parsedSubstrateSurfaces(fm: Record<string, unknown>): {
10
- on: string;
11
- at: string;
12
- match?: string[];
13
- }[];
10
+ export declare function parsedSubstrateSurfaces(fm: Record<string, unknown>): SurfaceEntry[];
@@ -1,6 +1,6 @@
1
1
  import { isDocKind, parseSubstrateFrontmatter, SURFACE_EVENTS, SURFACE_RUNGS } from './schema.js';
2
2
  const RETIRED_FIELDS = ['system-prompt-visibility', 'file-read-visibility', 'applies-to', 'read-when'];
3
- const SURFACE_ENTRY_KEYS = ['on', 'match', 'match-frontmatter', 'at'];
3
+ const SURFACE_ENTRY_KEYS = ['on', 'match', 'match-frontmatter', 'gate', 'at'];
4
4
  const SUPPRESSIBLE_RULES = ['length', 'broad-memory-read'];
5
5
  /** Strict validation of one raw routing entry. Runtime parsing deliberately
6
6
  * tolerates malformed docs; this authoring/preflight contract does not. */
@@ -8,15 +8,15 @@ export function lintSubstrateSurfaces(value) {
8
8
  if (value === undefined)
9
9
  return null;
10
10
  if (!Array.isArray(value))
11
- return `invalid surfaces: ${JSON.stringify(value)} (expected a list of {on, at, match?, match-frontmatter?} entries)`;
11
+ return `invalid surfaces: ${JSON.stringify(value)} (expected a list of {on, at, match?, match-frontmatter?, gate?} entries)`;
12
12
  for (const raw of value) {
13
13
  if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
14
- return `invalid surfaces entry: ${JSON.stringify(raw)} (expected an {on, at, match?, match-frontmatter?} object)`;
14
+ return `invalid surfaces entry: ${JSON.stringify(raw)} (expected an {on, at, match?, match-frontmatter?, gate?} object)`;
15
15
  }
16
16
  const entry = raw;
17
17
  for (const key of Object.keys(entry)) {
18
18
  if (!SURFACE_ENTRY_KEYS.includes(key)) {
19
- return `invalid surfaces entry: unknown key \`${key}\` (an entry carries only on, match, match-frontmatter, at)`;
19
+ return `invalid surfaces entry: unknown key \`${key}\` (an entry carries only on, match, match-frontmatter, gate, at)`;
20
20
  }
21
21
  }
22
22
  const on = entry['on'];
@@ -50,6 +50,10 @@ export function lintSubstrateSurfaces(value) {
50
50
  if ((on === 'read' || on === 'memory-read' || on === 'command') && !hasMatch && matchFrontmatter === undefined) {
51
51
  return `invalid surfaces entry: a \`${on}\` entry requires \`match\`${on === 'read' ? ' or `match-frontmatter`' : ''}`;
52
52
  }
53
+ const gate = entry['gate'];
54
+ if (gate !== undefined && (gate === null || typeof gate !== 'object' || Array.isArray(gate))) {
55
+ return `invalid surfaces entry \`gate\`: ${JSON.stringify(gate)} (expected a field→matcher object)`;
56
+ }
53
57
  }
54
58
  return null;
55
59
  }
@@ -1,4 +1,4 @@
1
- import type { SubstrateDoc } from './schema.js';
1
+ import type { SubstrateDoc, SurfaceEntry } from './schema.js';
2
2
  import type { NodeConfigSubject } from './subject-fields.js';
3
3
  /** Does `doc` pass its gate for `subject`?
4
4
  *
@@ -11,3 +11,6 @@ import type { NodeConfigSubject } from './subject-fields.js';
11
11
  * for this node; it remains findable by `crtr memory find` (search ignores
12
12
  * gate + rung). */
13
13
  export declare function gatePasses(doc: SubstrateDoc, subject: NodeConfigSubject): boolean;
14
+ /** Does a surface entry participate for `subject`? A missing subject cannot
15
+ * satisfy an entry gate; entries without one remain eligible. */
16
+ export declare function surfaceEntryGatePasses(entry: Pick<SurfaceEntry, 'gate'>, subject: NodeConfigSubject | null): boolean;
@@ -19,3 +19,8 @@ export function gatePasses(doc, subject) {
19
19
  return true;
20
20
  return evalCondition(doc.gate, subject);
21
21
  }
22
+ /** Does a surface entry participate for `subject`? A missing subject cannot
23
+ * satisfy an entry gate; entries without one remain eligible. */
24
+ export function surfaceEntryGatePasses(entry, subject) {
25
+ return entry.gate === undefined || (subject !== null && evalCondition(entry.gate, subject));
26
+ }