@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
@@ -9,6 +9,8 @@ export declare function resolveProfileSearchAction(bindings: BindingResolution<B
9
9
  * `crtr profile default set`, which rejects pinning a profile that doesn't
10
10
  * cover cwd (the selector would ignore/self-heal such a pin anyway). */
11
11
  export declare function profileCoversCwd(entry: ProfileEntry, cwd: string): boolean;
12
+ /** Resolve the headless selector against existing state without changing it. */
13
+ export declare function selectProfileForCwdReadOnly(cwd: string): string;
12
14
  /** Select the profile a node about to boot at `cwd` should run under.
13
15
  *
14
16
  * 1. `explicitProfile` present → resolve id/name via `loadProfileManifest`. If
@@ -93,6 +93,26 @@ function isMoreRecent(a, b) {
93
93
  function pickMostRecent(entries) {
94
94
  return entries.reduce((best, cur) => isMoreRecent(cur.manifest.last_used_at, best.manifest.last_used_at) ? cur : best);
95
95
  }
96
+ function profileSelectionState(cwd) {
97
+ const resolvedCwd = resolveCwd(cwd);
98
+ const covering = listProfiles().filter((profile) => profileCoversCwd(profile, resolvedCwd));
99
+ const exact = covering.filter((profile) => profile.manifest.projects.some((project) => project.path === resolvedCwd));
100
+ const pinnedId = getDefaultProfileId(resolvedCwd);
101
+ const pinned = pinnedId === null ? null : covering.find((profile) => profile.profileId === pinnedId) ?? null;
102
+ return { covering, exact, pinnedId, pinned };
103
+ }
104
+ /** Resolve the headless selector against existing state without changing it. */
105
+ export function selectProfileForCwdReadOnly(cwd) {
106
+ const { covering, exact, pinned } = profileSelectionState(cwd);
107
+ if (pinned !== null)
108
+ return pinned.profileId;
109
+ if (exact.length === 1)
110
+ return exact[0].profileId;
111
+ const pool = exact.length > 0 ? exact : covering;
112
+ if (pool.length > 0)
113
+ return pickMostRecent(pool).profileId;
114
+ return ROOT_PROFILE_ID;
115
+ }
96
116
  function isInteractive() {
97
117
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
98
118
  }
@@ -620,14 +640,11 @@ export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = fa
620
640
  updateProfileLastUsed(entry.profileId);
621
641
  return entry.profileId;
622
642
  }
623
- const covering = listProfiles().filter((p) => profileCoversCwd(p, resolvedCwd));
624
- const exact = covering.filter((p) => p.manifest.projects.some((proj) => proj.path === resolvedCwd));
643
+ const { covering, exact, pinnedId, pinned } = profileSelectionState(resolvedCwd);
625
644
  // A per-cwd pinned default (set from the menu with `d`) is the user's
626
645
  // explicit "default to THIS profile HERE". It outranks both the exact-single
627
646
  // auto-pick and global-MRU, so long as it still covers cwd; a stale pin
628
647
  // (profile deleted, or its dir removed from purview) is self-healed away.
629
- const pinnedId = getDefaultProfileId(resolvedCwd);
630
- const pinned = pinnedId !== null ? covering.find((p) => p.profileId === pinnedId) ?? null : null;
631
648
  if (pinnedId !== null && pinned === null)
632
649
  clearDefaultProfile(resolvedCwd);
633
650
  // A valid pin decides it outright and STOPS ASKING — the whole point of
@@ -9,7 +9,7 @@ import { operationIdContext } from '../../events/operation-id.js';
9
9
  import { buildRefInventory } from '../../memory/inline-ref-inventory.js';
10
10
  import { buildGuidance } from '../../memory/inline-ref-guidance.js';
11
11
  import { clearSessionCache } from '../../substrate/session-cache.js';
12
- import { captureSessionPromptTemplateChatSurfaces } from '../command-surface.js';
12
+ import { captureSessionChatCapabilities } from '../command-surface.js';
13
13
  import { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
14
14
  export { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
15
15
  import { configuredLadderProviders, isProviderPinnedModelToken, nextLadderModel, normalizeModel, } from '../launch.js';
@@ -1210,7 +1210,7 @@ export function createFrameDispatchContext(deps) {
1210
1210
  // cached docs. A FAILED reload (`.catch`) leaves both untouched: nothing
1211
1211
  // about the corpus is known to have changed.
1212
1212
  void currentSession()
1213
- .reload()
1213
+ .reload({ beforeSessionStart: () => captureSessionChatCapabilities(currentSession()) })
1214
1214
  .then(() => {
1215
1215
  // pi's reload() re-syncs queue modes from the shared settings.json
1216
1216
  // (its ONE post-bind syncQueueModesFromSettings call site) —
@@ -1221,9 +1221,6 @@ export function createFrameDispatchContext(deps) {
1221
1221
  agent.steeringMode = 'all';
1222
1222
  agent.followUpMode = 'all';
1223
1223
  }
1224
- // `session.reload()` has replaced pi's loaded template objects;
1225
- // capture their crouter-only metadata before any observer refresh.
1226
- captureSessionPromptTemplateChatSurfaces(currentSession());
1227
1224
  memoizedRefInventory = null;
1228
1225
  clearSessionCache();
1229
1226
  registry.forEachHelloed((observer) => {
@@ -1,5 +1,4 @@
1
- import type { ChatInventoryDTO, ChatInventoryMemoryRefDTO } from '../../api/dto/chat-inventory.js';
2
- export declare function gatewayMemoryRows(refs: unknown): ChatInventoryMemoryRefDTO[];
1
+ import type { ChatInventoryDTO } from '../../api/dto/chat-inventory.js';
3
2
  /** Collect the chat inventories for one node. Always resolves — every failure
4
3
  * mode is expressed in the returned document, not thrown. */
5
4
  export declare function readChatInventory(nodeId: string): Promise<ChatInventoryDTO>;
@@ -24,7 +24,7 @@
24
24
  import { randomUUID } from 'node:crypto';
25
25
  import { getNode } from '../canvas/canvas.js';
26
26
  import { BrokerClient } from '../broker-client/index.js';
27
- import { isDeterministicCommandExpansion } from './command-expansion.js';
27
+ import { gatewayCommandRowsFromDetail, gatewayMemoryRows } from './chat-inventory-rows.js';
28
28
  import { isBrokerLive } from './model-swap.js';
29
29
  /** Same budget as `broker-request.ts`: this is local engine work, so it is
30
30
  * generous slack rather than a tuned deadline. One budget covers BOTH answers —
@@ -36,81 +36,6 @@ const READ_TIMEOUT_MS = 15_000;
36
36
  * throws away — on every activation and reconnect, worst on the longest
37
37
  * conversations. One entry is the smallest legal request. */
38
38
  const SNAPSHOT_TAIL = 1;
39
- const MEMORY_KINDS = new Set(['knowledge', 'preference']);
40
- const MEMORY_SCOPES = new Set(['node', 'project', 'profile', 'user', 'builtin']);
41
- function nonEmptyString(value) {
42
- return typeof value === 'string' && value !== '';
43
- }
44
- /** Keep the rows a chat surface can actually use and drop the rest. Malformed
45
- * rows are discarded individually so one bad registration cannot take down the
46
- * whole inventory.
47
- *
48
- * This validation lives here rather than reusing the attach viewer's parser:
49
- * `src/core` does not import `src/clients`, and adding that import would invert
50
- * the layering. */
51
- function commandRows(detail) {
52
- if (detail === undefined)
53
- return [];
54
- let parsed;
55
- try {
56
- parsed = JSON.parse(detail);
57
- }
58
- catch {
59
- return [];
60
- }
61
- if (!Array.isArray(parsed))
62
- return [];
63
- const out = [];
64
- for (const row of parsed) {
65
- if (typeof row !== 'object' || row === null)
66
- continue;
67
- const record = row;
68
- if (record['gatewayCapable'] !== true)
69
- continue;
70
- const source = record['source'];
71
- if (source !== 'command' && source !== 'template')
72
- continue;
73
- if (!nonEmptyString(record['name']) || typeof record['description'] !== 'string')
74
- continue;
75
- const expansion = record['expansion'];
76
- const template = record['template'];
77
- const argumentHint = record['argumentHint'];
78
- out.push({
79
- name: record['name'],
80
- description: record['description'],
81
- source,
82
- ...(nonEmptyString(argumentHint) ? { argument_hint: argumentHint } : {}),
83
- ...(isDeterministicCommandExpansion(expansion) ? { expansion } : {}),
84
- ...(typeof template === 'string' ? { template } : {}),
85
- });
86
- }
87
- return out;
88
- }
89
- export function gatewayMemoryRows(refs) {
90
- if (!Array.isArray(refs))
91
- return [];
92
- const out = [];
93
- for (const row of refs) {
94
- if (typeof row !== 'object' || row === null)
95
- continue;
96
- const record = row;
97
- if (record['gatewayVisible'] !== true)
98
- continue;
99
- if (!nonEmptyString(record['name']) || typeof record['shortForm'] !== 'string')
100
- continue;
101
- if (typeof record['kind'] !== 'string' || !MEMORY_KINDS.has(record['kind']))
102
- continue;
103
- if (typeof record['scope'] !== 'string' || !MEMORY_SCOPES.has(record['scope']))
104
- continue;
105
- out.push({
106
- name: record['name'],
107
- kind: record['kind'],
108
- scope: record['scope'],
109
- shortForm: record['shortForm'],
110
- });
111
- }
112
- return out;
113
- }
114
39
  const empty = (nodeId) => ({
115
40
  node_id: nodeId,
116
41
  broker_live: false,
@@ -174,7 +99,7 @@ export function readChatInventory(nodeId) {
174
99
  client.on('frame', (frame) => {
175
100
  // `get_commands` carries no id, so its ack is matched on `for` alone.
176
101
  if (frame.type === 'ack' && frame.for === 'get_commands') {
177
- commands = frame.ok ? commandRows(frame.detail) : [];
102
+ commands = frame.ok ? gatewayCommandRowsFromDetail(frame.detail) : [];
178
103
  settleWhenComplete();
179
104
  return;
180
105
  }
@@ -44,7 +44,7 @@ export function transitionGuidanceFromBrokerState(node, from, to) {
44
44
  sections.push(to.mode === 'orchestrator' ? orchestrationGuidance(node) : baseModeGuidance());
45
45
  }
46
46
  if (from.lifecycle !== to.lifecycle) {
47
- sections.push(loadMemoryBody(to.lifecycle === 'resident' ? 'lifecycle/resident' : 'lifecycle/terminal'));
47
+ sections.push(loadMemoryBody(to.lifecycle === 'resident' ? 'turn-lifecycle/resident' : 'turn-lifecycle/terminal'));
48
48
  }
49
49
  return sections.join('\n\n');
50
50
  }
@@ -31,7 +31,7 @@ import { FRONT_DOOR_ENV } from './front-door.js';
31
31
  import { CANVAS_EXTENSIONS, piInvocationToSdkConfig, } from './launch.js';
32
32
  import { assertEngineVersion, loadBrokerEngine } from './broker-sdk.js';
33
33
  import { resolveBundledPiPackageDir } from './pi-cli.js';
34
- import { captureSessionPromptTemplateChatSurfaces } from './command-surface.js';
34
+ import { captureSessionChatCapabilities } from './command-surface.js';
35
35
  import { PoolCredentialStore } from '../pool-credential-store.js';
36
36
  import { clearCleanAbort, markBusy, markCleanAbort } from './busy.js';
37
37
  import { recordFaultForCurrentNode } from './fault.js';
@@ -899,9 +899,9 @@ export async function buildBrokerSession(engine, cfg) {
899
899
  sessionStartEvent: o.sessionStartEvent,
900
900
  });
901
901
  installAdvertisedCommandInvocationContract(created.session);
902
- // The resource loader has completed its initial template load. Capture
903
- // crouter-only template metadata before command disclosure can observe it.
904
- captureSessionPromptTemplateChatSurfaces(created.session);
902
+ // The resource loader has completed its initial load, but extensions have
903
+ // not received session_start yet. Freeze the Gateway capability boundary.
904
+ captureSessionChatCapabilities(created.session);
905
905
  // CONSTRUCTION-ONLY MASK — see modelLessRegistry: reveal now that model
906
906
  // selection (construction-time only) is done, so a later /login re-drive's
907
907
  // list_models/cycle_model/provider-rotation see the real registry.
@@ -0,0 +1,8 @@
1
+ import type { ChatInventoryCommandDTO, ChatInventoryMemoryRefDTO } from '../../api/dto/chat-inventory.js';
2
+ /** Keep the command rows a Gateway can use. Malformed registrations are
3
+ * discarded individually so one producer cannot take down the inventory. */
4
+ export declare function gatewayCommandRows(rows: unknown): ChatInventoryCommandDTO[];
5
+ export declare function chatInventoryCommandRows(rows: unknown): ChatInventoryCommandDTO[];
6
+ export declare function gatewayCommandRowsFromDetail(detail: string | undefined): ChatInventoryCommandDTO[];
7
+ export declare function chatInventoryMemoryRows(rows: unknown): ChatInventoryMemoryRefDTO[];
8
+ export declare function gatewayMemoryRows(refs: unknown): ChatInventoryMemoryRefDTO[];
@@ -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>;