@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,27 @@
1
+ // The `crtr.human-completion/v1` document handed to a completion action on
2
+ // stdin. Composed at settlement from the canonical terminal result, so the
3
+ // delivered document reflects the settlement instant and the delivery lane
4
+ // never reads ticket storage.
5
+ /** The one place a cancellation record without `disposition` reads as
6
+ * `canceled`. Every other reader treats both dispositions alike. */
7
+ export function completionEventFor(result) {
8
+ if (result.kind === 'canceled')
9
+ return result.disposition ?? 'canceled';
10
+ return 'answered';
11
+ }
12
+ /** Build the complete document with `delivery.attempt` as the lane's placeholder. */
13
+ export function composeHumanCompletion(args) {
14
+ const { requestId, result, binding, source } = args;
15
+ const event = completionEventFor(result);
16
+ const actor = result.actor;
17
+ return {
18
+ schema: 'crtr.human-completion/v1',
19
+ requestId,
20
+ event,
21
+ settledAt: result.kind === 'page' ? result.completedAt : result.canceledAt,
22
+ ...(result.kind === 'page' ? { responses: result.responses } : {}),
23
+ action: { name: binding.name, payload: binding.payload },
24
+ provenance: { source, ...(actor === undefined ? {} : { actor }) },
25
+ delivery: { attempt: 0 },
26
+ };
27
+ }
@@ -8,6 +8,8 @@ export declare function responsePath(dir: string): string;
8
8
  export declare function progressPath(dir: string): string;
9
9
  export declare function claimPath(dir: string): string;
10
10
  export declare function replyRoutePath(dir: string): string;
11
+ /** The frozen completion-action binding, written once at request creation. */
12
+ export declare function actionBindingPath(dir: string): string;
11
13
  export interface TicketReplyRoute {
12
14
  bridge_node_id: string;
13
15
  }
@@ -10,6 +10,8 @@ export function responsePath(dir) { return `${dir}/response.json`; }
10
10
  export function progressPath(dir) { return `${dir}/progress.json`; }
11
11
  export function claimPath(dir) { return `${dir}/claim.json`; }
12
12
  export function replyRoutePath(dir) { return `${dir}/reply-route.json`; }
13
+ /** The frozen completion-action binding, written once at request creation. */
14
+ export function actionBindingPath(dir) { return `${dir}/action.json`; }
13
15
  /** A reply route exists only when completing the ticket must finish a human
14
16
  * bridge and fan its result back to subscribers. Its absence is the complete
15
17
  * representation of a standalone page. */
@@ -19,9 +19,22 @@ export interface PreparedPage {
19
19
  module?: string;
20
20
  manifest: PageManifest;
21
21
  }
22
- /** Validate one authored page source into publishable form. Reads only: a page
23
- * that fails authoring leaves nothing behind, so a caller that must create a
24
- * bridge node prepares first and creates it only once the page is known good.
22
+ export interface PreparePageSourceOptions {
23
+ /** `jsx` carries .tsx semantics; `html` is a complete document. */
24
+ dialect: 'jsx' | 'html';
25
+ /** The complete authored page text, inline. */
26
+ document: string;
27
+ source?: PageSource;
28
+ productKinds?: ProductPageComponents;
29
+ delivery?: PageDelivery;
30
+ }
31
+ /** Validate one authored page document into publishable form. The single
32
+ * validation path: `preparePage` reads a file and lands here, and a
33
+ * programmatic request supplies the same text inline. */
34
+ export declare function preparePageSource(opts: PreparePageSourceOptions): PreparedPage;
35
+ /** Validate one authored page source file into publishable form. Reads only: a
36
+ * page that fails authoring leaves nothing behind, so a caller that must create
37
+ * a bridge node prepares first and creates it only once the page is known good.
25
38
  * The source is read exactly once, so the published document is the same bytes
26
39
  * the manifest was derived from even if the file changes underneath. */
27
40
  export declare function preparePage(opts: Omit<SubmitPageOptions, 'dir'>): PreparedPage;
@@ -81,6 +94,8 @@ export interface PageTicket {
81
94
  export declare function readPageTicket(dir: string): PageTicket;
82
95
  export interface CompletePageOptions {
83
96
  completedAt?: string;
97
+ /** Recorded when an authorized programmatic writer answered instead of a person. */
98
+ actor?: string;
84
99
  }
85
100
  export declare function completePage(dir: string, responses: unknown, claimToken: string, opts?: CompletePageOptions): {
86
101
  won: boolean;
@@ -94,11 +109,15 @@ export declare function takeoverAndCompletePage(dir: string, responses: unknown,
94
109
  won: boolean;
95
110
  result: TicketResult;
96
111
  };
97
- /** Cancellation races finalization but never requires or removes another claim. */
98
- export declare function cancelTicket(dir: string, opts?: {
112
+ export interface CancelTicketOptions {
99
113
  reason?: string;
100
114
  actor?: string;
101
- }): {
115
+ /** The recipient closing it writes `dismissed`; the requester withdrawing it
116
+ * writes `canceled`. Omitted where the caller is neither. */
117
+ disposition?: 'canceled' | 'dismissed';
118
+ }
119
+ /** Cancellation races finalization but never requires or removes another claim. */
120
+ export declare function cancelTicket(dir: string, opts?: CancelTicketOptions): {
102
121
  status: 'canceled' | 'already_resolved';
103
122
  result: TicketResult;
104
123
  };
@@ -23,10 +23,10 @@ const feedbackSchema = z.object({ file: z.string().min(1).refine(isAbsolute, 'fi
23
23
  });
24
24
  });
25
25
  const slotResponseSchema = z.record(z.string(), z.unknown());
26
- const pageResultSchema = z.object({ schema: z.literal('crtr.page-response/v1'), kind: z.literal('page'), responses: z.record(z.string(), slotResponseSchema), summary: z.string(), completedAt: iso }).strict();
26
+ const pageResultSchema = z.object({ schema: z.literal('crtr.page-response/v1'), kind: z.literal('page'), responses: z.record(z.string(), slotResponseSchema), summary: z.string(), completedAt: iso, actor: z.string().optional() }).strict();
27
27
  const pageProgressSchema = z.object({ kind: z.literal('page'), responses: z.record(z.string(), slotResponseSchema), savedAt: iso }).strict();
28
28
  const reviewResultSchema = z.object({ schema: z.literal('humanloop.review-response/v1'), kind: z.literal('review'), result: feedbackSchema, completedAt: iso }).strict();
29
- const canceledResultSchema = z.object({ schema: z.literal('humanloop.cancel/v1'), kind: z.literal('canceled'), canceledAt: iso, reason: z.string().optional(), actor: z.string().optional() }).strict();
29
+ const canceledResultSchema = z.object({ schema: z.literal('humanloop.cancel/v1'), kind: z.literal('canceled'), canceledAt: iso, reason: z.string().optional(), actor: z.string().optional(), disposition: z.enum(['canceled', 'dismissed']).optional() }).strict();
30
30
  /** Strict decoder for the only canonical final marker. */
31
31
  export function readTicketResult(dir) {
32
32
  let raw;
@@ -52,9 +52,19 @@ function publishRequest(path, value) {
52
52
  if (!publishJsonExclusive(path, value))
53
53
  throw new Error(`ticket request already exists: ${path}`);
54
54
  }
55
- /** Validate one authored page source into publishable form. Reads only: a page
56
- * that fails authoring leaves nothing behind, so a caller that must create a
57
- * bridge node prepares first and creates it only once the page is known good.
55
+ /** Validate one authored page document into publishable form. The single
56
+ * validation path: `preparePage` reads a file and lands here, and a
57
+ * programmatic request supplies the same text inline. */
58
+ export function preparePageSource(opts) {
59
+ const stamped = { ...(opts.source ?? {}), emittedAt: opts.source?.emittedAt ?? new Date().toISOString() };
60
+ const delivery = opts.delivery ?? { placement: 'inline', inbox: false, reply: false };
61
+ if (opts.dialect === 'html')
62
+ return { document: opts.document, manifest: deriveHtmlPageManifest(opts.document, stamped, delivery) };
63
+ return { document: opts.document, module: compilePage(opts.document), manifest: derivePageManifestFromDocument(opts.document, opts.productKinds ?? [], stamped, delivery) };
64
+ }
65
+ /** Validate one authored page source file into publishable form. Reads only: a
66
+ * page that fails authoring leaves nothing behind, so a caller that must create
67
+ * a bridge node prepares first and creates it only once the page is known good.
58
68
  * The source is read exactly once, so the published document is the same bytes
59
69
  * the manifest was derived from even if the file changes underneath. */
60
70
  export function preparePage(opts) {
@@ -63,12 +73,8 @@ export function preparePage(opts) {
63
73
  throw new PageAuthoringError(`page source file must be .tsx, .html, or .htm: ${opts.sourceFile}`);
64
74
  if (!existsSync(opts.sourceFile))
65
75
  throw new PageAuthoringError(`page source file does not exist: ${opts.sourceFile}`);
66
- const document = readFileSync(opts.sourceFile, 'utf8');
67
- const stamped = { ...(opts.source ?? {}), emittedAt: opts.source?.emittedAt ?? new Date().toISOString() };
68
- const delivery = opts.delivery ?? { placement: 'inline', inbox: false, reply: false };
69
- if (html)
70
- return { document, manifest: deriveHtmlPageManifest(document, stamped, delivery) };
71
- return { document, module: compilePage(document), manifest: derivePageManifestFromDocument(document, opts.productKinds ?? [], stamped, delivery) };
76
+ const { sourceFile: _sourceFile, ...rest } = opts;
77
+ return preparePageSource({ ...rest, dialect: html ? 'html' : 'jsx', document: readFileSync(opts.sourceFile, 'utf8') });
72
78
  }
73
79
  /** Remove a stale generation file. The manifest always switches first, so a
74
80
  * failed cleanup leaves one valid current record plus harmless old source. */
@@ -237,7 +243,7 @@ function completePageLocked(dir, responses, claimToken, opts) {
237
243
  requireClaimOwnership(dir, claimToken);
238
244
  const manifest = requirePage(dir);
239
245
  const parsedResponses = validateFinalPageTicketResponses(dir, responses);
240
- const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses), completedAt: opts.completedAt ?? new Date().toISOString() };
246
+ const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses), completedAt: opts.completedAt ?? new Date().toISOString(), ...(opts.actor === undefined ? {} : { actor: opts.actor }) };
241
247
  const won = exclusiveResult(dir, result);
242
248
  clearOwnedWork(dir, claimToken);
243
249
  return { won, result: won ? result : readTicketResult(dir) ?? result };
@@ -280,7 +286,7 @@ export function cancelTicket(dir, opts = {}) {
280
286
  const canceled = withTicketLock(ticket, () => {
281
287
  if (existsSync(pageManifestPath(ticket)))
282
288
  requireCancelablePage(ticket);
283
- const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: new Date().toISOString(), ...(opts.reason === undefined ? {} : { reason: opts.reason }), ...(opts.actor === undefined ? {} : { actor: opts.actor }) };
289
+ const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: new Date().toISOString(), ...(opts.reason === undefined ? {} : { reason: opts.reason }), ...(opts.actor === undefined ? {} : { actor: opts.actor }), ...(opts.disposition === undefined ? {} : { disposition: opts.disposition }) };
284
290
  const won = exclusiveResult(ticket, result);
285
291
  return { status: won ? 'canceled' : 'already_resolved', result: won ? result : readTicketResult(ticket) ?? result };
286
292
  });
@@ -89,6 +89,8 @@ export interface PageTicketResult {
89
89
  responses: PageResponses;
90
90
  summary: string;
91
91
  completedAt: string;
92
+ /** Recorded when an authorized programmatic writer answered instead of a person. */
93
+ actor?: string;
92
94
  }
93
95
  export interface ReviewTicketResult {
94
96
  schema: 'humanloop.review-response/v1';
@@ -102,6 +104,9 @@ export interface CanceledTicketResult {
102
104
  canceledAt: string;
103
105
  reason?: string;
104
106
  actor?: string;
107
+ /** Who ended it without an answer: the recipient (`dismissed`) or the
108
+ * requester (`canceled`). Absent on records written before the field. */
109
+ disposition?: 'canceled' | 'dismissed';
105
110
  }
106
111
  /** The sole canonical response.json union. */
107
112
  export type TicketResult = PageTicketResult | ReviewTicketResult | CanceledTicketResult;
@@ -0,0 +1,25 @@
1
+ export interface ResolvedHumanAction {
2
+ name: string;
3
+ /** Absolute path of the config file that won resolution. */
4
+ configOrigin: string;
5
+ /** Fully resolved argv; argv[0] is an absolute executable path. */
6
+ argv: string[];
7
+ /** Fully resolved absolute working directory. */
8
+ cwd: string;
9
+ }
10
+ export type HumanActionIssue = {
11
+ kind: 'unknown_action';
12
+ name: string;
13
+ } | {
14
+ kind: 'invalid_action';
15
+ name: string;
16
+ configOrigin: string;
17
+ reason: string;
18
+ block?: true;
19
+ };
20
+ /** Resolve one declared action from a creator's absolute cwd: nearest project
21
+ * ancestor first, then farther project ancestors, then user scope. Never
22
+ * consults plugin or profile config. Read-only; never executes anything. */
23
+ export declare function resolveHumanAction(name: string, creatorCwd: string): ResolvedHumanAction | HumanActionIssue;
24
+ /** Every declared action reachable from a cwd, with its issues, for doctor. */
25
+ export declare function inspectHumanActions(creatorCwd: string): Array<ResolvedHumanAction | HumanActionIssue>;
@@ -0,0 +1,101 @@
1
+ import { statSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { CONFIG_FILE } from '../types.js';
4
+ import { humanActionDeclarationIssues, readRawConfigAtRoot } from './config.js';
5
+ import { findProjectScopeRoots, scopeRoot } from './scope.js';
6
+ function actionContributors(creatorCwd) {
7
+ const contributors = [];
8
+ for (const scopeRootPath of findProjectScopeRoots(creatorCwd, null)) {
9
+ const raw = readRawConfigAtRoot(scopeRootPath)?.humanActions;
10
+ if (raw !== undefined) {
11
+ contributors.push({
12
+ configOrigin: join(scopeRootPath, CONFIG_FILE),
13
+ authoringRoot: dirname(scopeRootPath),
14
+ raw,
15
+ });
16
+ }
17
+ }
18
+ const userRoot = scopeRoot('user');
19
+ if (userRoot !== null) {
20
+ const raw = readRawConfigAtRoot(userRoot)?.humanActions;
21
+ if (raw !== undefined) {
22
+ contributors.push({ configOrigin: join(userRoot, CONFIG_FILE), authoringRoot: userRoot, raw });
23
+ }
24
+ }
25
+ return contributors;
26
+ }
27
+ function declarationIssue(name, raw) {
28
+ return humanActionDeclarationIssues({ [name]: raw }).find((issue) => issue.name === name)?.reason;
29
+ }
30
+ function resolveDeclaredAction(name, declaration, contributor) {
31
+ const cwd = resolve(contributor.authoringRoot, declaration.cwd);
32
+ try {
33
+ if (!statSync(cwd).isDirectory()) {
34
+ return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `cwd is not a directory: ${cwd}` };
35
+ }
36
+ }
37
+ catch {
38
+ return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `cwd does not exist: ${cwd}` };
39
+ }
40
+ const executable = resolve(contributor.authoringRoot, declaration.argv[0]);
41
+ try {
42
+ const stat = statSync(executable);
43
+ if (!stat.isFile()) {
44
+ return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `argv[0] is not a regular file: ${executable}` };
45
+ }
46
+ if ((stat.mode & 0o111) === 0) {
47
+ return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `argv[0] lacks the POSIX exec bit: ${executable}` };
48
+ }
49
+ }
50
+ catch {
51
+ return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `argv[0] does not exist: ${executable}` };
52
+ }
53
+ return {
54
+ name,
55
+ configOrigin: contributor.configOrigin,
56
+ argv: [executable, ...declaration.argv.slice(1)],
57
+ cwd,
58
+ };
59
+ }
60
+ /** Resolve one declared action from a creator's absolute cwd: nearest project
61
+ * ancestor first, then farther project ancestors, then user scope. Never
62
+ * consults plugin or profile config. Read-only; never executes anything. */
63
+ export function resolveHumanAction(name, creatorCwd) {
64
+ for (const contributor of actionContributors(creatorCwd)) {
65
+ if (contributor.raw === null || typeof contributor.raw !== 'object' || Array.isArray(contributor.raw))
66
+ continue;
67
+ const declarations = contributor.raw;
68
+ if (!Object.hasOwn(declarations, name))
69
+ continue;
70
+ const raw = declarations[name];
71
+ const issue = declarationIssue(name, raw);
72
+ if (issue !== undefined) {
73
+ return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: issue };
74
+ }
75
+ return resolveDeclaredAction(name, raw, contributor);
76
+ }
77
+ return { kind: 'unknown_action', name };
78
+ }
79
+ /** Every declared action reachable from a cwd, with its issues, for doctor. */
80
+ export function inspectHumanActions(creatorCwd) {
81
+ const results = [];
82
+ for (const contributor of actionContributors(creatorCwd)) {
83
+ const issues = humanActionDeclarationIssues(contributor.raw);
84
+ const invalid = new Map(issues.map((issue) => [issue.name, issue.reason]));
85
+ if (contributor.raw === null || typeof contributor.raw !== 'object' || Array.isArray(contributor.raw)) {
86
+ for (const issue of issues) {
87
+ results.push({ kind: 'invalid_action', name: issue.name, configOrigin: contributor.configOrigin, reason: issue.reason, ...(issue.block === true ? { block: true } : {}) });
88
+ }
89
+ continue;
90
+ }
91
+ for (const [name, raw] of Object.entries(contributor.raw)) {
92
+ const issue = invalid.get(name);
93
+ if (issue !== undefined) {
94
+ results.push({ kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: issue });
95
+ continue;
96
+ }
97
+ results.push(resolveDeclaredAction(name, raw, contributor));
98
+ }
99
+ }
100
+ return results;
101
+ }
@@ -313,7 +313,7 @@ function matchNormalizedChild(dir, segment, want, ext = '.md') {
313
313
  * directory by normalized name, then resolve the final segment as either a
314
314
  * `.md` file or a directory. Either half is null when
315
315
  * the segment path does not resolve that way. This is what makes
316
- * `00-runtime-base.md` findable as `runtime-base` and `01-spine/00-has-manager`
316
+ * `00-runtime-base/00-authoring.md` findable as `runtime-base/authoring` and `01-spine/00-has-manager`
317
317
  * findable as `spine/has-manager` — the physical path keeps its pins, only
318
318
  * lookup is prefix-blind. */
319
319
  function resolveNormalizedPath(baseDir, segments, ext = '.md') {
@@ -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[];