@north-light/crouter 0.3.216 → 0.3.218

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 (70) hide show
  1. package/dist/api/client.d.ts +5 -2
  2. package/dist/api/client.js +7 -1
  3. package/dist/api/dto/canvas.d.ts +9 -0
  4. package/dist/api/dto/nodes.d.ts +27 -1
  5. package/dist/builtin-memory/04-base-worker.md +7 -1
  6. package/dist/builtin-memory/internal/plugins.md +67 -0
  7. package/dist/builtin-memory/memory-read-orientation.md +13 -0
  8. package/dist/clients/attach/chrome/bash-jobs.js +1 -1
  9. package/dist/clients/attach/photon_rs_bg.wasm +0 -0
  10. package/dist/clients/attach/viewer.js +516 -516
  11. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +2 -2
  12. package/dist/clients/inbox/tui/input.js +12 -8
  13. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  14. package/dist/clients/inbox/tui/slots.js +9 -3
  15. package/dist/commands/human/shared.js +1 -1
  16. package/dist/commands/memory/edit.js +6 -1
  17. package/dist/commands/memory/lint.d.ts +1 -6
  18. package/dist/commands/memory/lint.js +18 -126
  19. package/dist/commands/memory/list.d.ts +1 -0
  20. package/dist/commands/memory/list.js +17 -2
  21. package/dist/commands/memory/read.js +15 -6
  22. package/dist/commands/memory/shared.d.ts +33 -3
  23. package/dist/commands/memory/shared.js +180 -1
  24. package/dist/commands/memory/write.js +6 -1
  25. package/dist/commands/pkg/browse/catalog.js +2 -0
  26. package/dist/commands/pkg/browse/model.d.ts +4 -1
  27. package/dist/commands/pkg/plugin-manage.js +209 -148
  28. package/dist/core/__tests__/human-cancel-guard.test.js +30 -12
  29. package/dist/core/bash-jobs.d.ts +2 -5
  30. package/dist/core/bash-jobs.js +4 -8
  31. package/dist/core/command-plugins/bundle.d.ts +4 -1
  32. package/dist/core/command-plugins/bundle.js +16 -2
  33. package/dist/core/human/__tests__/page.test.js +12 -0
  34. package/dist/core/human/component-docs.js +6 -1
  35. package/dist/core/human/page-schema.d.ts +14 -0
  36. package/dist/core/human/page-schema.js +24 -7
  37. package/dist/core/human/page.d.ts +2 -0
  38. package/dist/core/human/page.js +17 -6
  39. package/dist/core/human/scan.d.ts +5 -4
  40. package/dist/core/human/scan.js +7 -4
  41. package/dist/core/human/tickets.js +9 -2
  42. package/dist/core/io.js +3 -2
  43. package/dist/core/manifest.d.ts +2 -0
  44. package/dist/core/manifest.js +5 -0
  45. package/dist/core/memory/extensions.d.ts +30 -0
  46. package/dist/core/memory/extensions.js +219 -0
  47. package/dist/core/preview-result-path.d.ts +4 -0
  48. package/dist/core/preview-result-path.js +25 -0
  49. package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
  50. package/dist/core/substrate/frontmatter-validation.js +101 -0
  51. package/dist/core/substrate/index.d.ts +1 -0
  52. package/dist/core/substrate/index.js +1 -0
  53. package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
  54. package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
  55. package/dist/daemon/api/handlers/canvas.js +3 -0
  56. package/dist/daemon/api/handlers/inbox.js +4 -3
  57. package/dist/daemon/api/handlers/nodes.d.ts +1 -0
  58. package/dist/daemon/api/handlers/nodes.js +101 -19
  59. package/dist/daemon/api/handlers/reports.d.ts +4 -0
  60. package/dist/daemon/api/handlers/reports.js +14 -8
  61. package/dist/daemon/crtrd.js +7 -5
  62. package/dist/daemon/manage.d.ts +3 -0
  63. package/dist/daemon/manage.js +14 -0
  64. package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
  65. package/dist/pi-extensions/canvas-bash-valve.js +19 -6
  66. package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
  67. package/dist/pi-extensions/canvas-preview-result.js +9 -23
  68. package/dist/types.d.ts +31 -0
  69. package/package.json +1 -1
  70. package/runtime.lock.json +2 -2
@@ -78,3 +78,15 @@ test('final and partial response validation share slot-addressed rules', () => {
78
78
  assert.throws(() => validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['maybe'], comments: [] } }), /unknown option id "maybe"/);
79
79
  assert.throws(() => validatePartialPageResponses(manifest, { extra: {} }), /unknown or non-response-bearing slot id "extra"/);
80
80
  });
81
+ // Which questions have a minimum is the author's call, and both defaults matter to a
82
+ // renderer mirroring this rule: pick-many is answered by ticking nothing, pick-one is not.
83
+ test('an empty answer is refused only where the question is required', () => {
84
+ const manifest = derivePageManifestFromDocument(page(`<UserQuestion id="risks" label="Tick everything that applies" body="Skip it if none do." mode="multi" options={[{ id: "data", label: "Data" }]} />
85
+ <UserQuestion id="lane" label="Which lane?" body="Ships tonight." mode="single" required={false} options={[{ id: "now", label: "Now" }]} />
86
+ <UserText id="window" label="Maintenance window" initialText="" required />`));
87
+ const empty = { risks: { selectedOptionIds: [], comments: [] }, lane: { selectedOptionIds: [], comments: [] }, window: { text: 'Sunday 02:00', edited: true } };
88
+ assert.deepEqual(validatePageResponses(manifest, empty), empty);
89
+ assert.throws(() => validatePageResponses(manifest, { ...empty, window: { text: ' ', edited: true } }), /slot "window" is required and was left empty/);
90
+ const required = derivePageManifestFromDocument(page('<UserQuestion id="risks" label="Tick everything that applies" body="At least one." mode="multi" required options={[{ id: "data", label: "Data" }]} />'));
91
+ assert.throws(() => validatePageResponses(required, { risks: { selectedOptionIds: [], comments: [] } }), /multi-select slot "risks" requires a selection/);
92
+ });
@@ -55,6 +55,7 @@ Props
55
55
  Rules
56
56
  - \`<Step>\` elements are children of \`<Page>\`; step order is source order, and the ticket manifest records the step count.
57
57
  - A single-step page needs no \`<Step>\` at all — put the content directly in \`<Page>\`.
58
+ - The reader sees one step at a time, so hold each step to one question or request for action, with the context that decision needs beside it. Several decisions means several steps, never one long scroll.
58
59
 
59
60
  Example
60
61
  \`\`\`jsx
@@ -79,12 +80,13 @@ Props
79
80
  - \`options\` — required nonempty array of \`{id:string (nonempty, unique), label:string (nonempty), description?:string, recommended?:boolean}\`.
80
81
  - \`recommended\` on one option marks it the suggested answer. At most one option per question may set it. The inbox shows it on the row, and when this question is the page's only response-bearing component the user can accept it from the list with one keystroke, without opening the page. Recommend when you have a view worth acting on; leave it off when the choice is genuinely theirs.
81
82
  - \`mode\` — required: \`single\` or \`multi\`.
83
+ - \`required?:boolean\` — whether an empty answer is refused. Defaults to the mode: a \`single\` question must be answered, a \`multi\` one need not be, because leaving every box unticked says "none of these apply". Set \`required\` when the mode's default is wrong — \`required={false}\` on a \`single\` question the user may skip, \`required\` on a \`multi\` one where at least one tick is the point.
82
84
  - \`label\` — required nonempty string: the question itself, drawn as the bold first line.
83
85
  - \`body\` — required nonempty string: markdown rendered above the choices — the context under the question a label cannot carry. Display-only; contributes nothing to the response.
84
86
  - \`allowFreetext?:boolean\`, \`freetextLabel?:string\`, \`freetextPlaceholder?:string\` — adds a final "something else" choice, drawn as one more row with its own control and a field to write in rather than as a note on the question. It is exclusive on a \`single\` question (choosing it clears the selection, and choosing an option clears what was written) and just another box on a \`multi\` one. \`freetextLabel\` names that row, default "Something else".
85
87
 
86
88
  Response rules
87
- - \`selectedOptionIds\` contains unique known option ids. A \`single\` response has at most one selected id and requires exactly one selection or nonempty allowed \`freetext\`; a \`multi\` response requires a selection, comment, or nonempty allowed \`freetext\`.
89
+ - \`selectedOptionIds\` contains unique known option ids; a \`single\` response has at most one. When the question is required, a \`single\` response needs exactly one selection or nonempty allowed \`freetext\`, and a \`multi\` response needs a selection, comment, or nonempty allowed \`freetext\`.
88
90
  - \`freetext\` is accepted only when \`allowFreetext:true\`.
89
91
  - ${COMMENT_SHAPE} UserQuestion accepts only \`option\` anchors; \`optionId\` must name a configured option.
90
92
 
@@ -116,6 +118,7 @@ Props
116
118
  - \`label?:string\` — the component's label; required by the multi-response page rule.
117
119
  - \`placeholder?:string\` — the hint shown while the field is empty.
118
120
  - \`singleLine?:boolean\` — swaps the writing surface for one compact form field.
121
+ - \`required?:boolean\` — defaults false. When set, a field left empty (whitespace only) is refused, and the inbox marks it unanswered rather than sending it.
119
122
 
120
123
  The field is always writable; there is no read-only mode, and the config rejects an \`editable\` prop.
121
124
 
@@ -140,6 +143,7 @@ Props
140
143
  - \`rows?:\` array of \`{id:string (nonempty, unique), cells:Record<string,string|number|boolean|null>}\`. Rows are inline — map your data into this array in the module.
141
144
  - \`rowSelect?:"none"|"single"|"multi"\` — defaults to \`none\`.
142
145
  - \`columnSelect?:"none"|"single"|"multi"\` — defaults to \`none\`.
146
+ - \`required?:boolean\` — defaults false. When set on a selectable table, a response selecting no row and no column is refused.
143
147
  - \`label?:string\` — the component's label; required by the multi-response page rule when the table is selectable.
144
148
  - \`body?:string\` — optional markdown rendered above the table. Display-only; it contributes nothing to the response.
145
149
 
@@ -169,6 +173,7 @@ Props
169
173
  - \`cards?:\` array of \`{id:string (nonempty, unique), title:string (nonempty), subtitle?:string, body?:string, tag?:string}\`. Cards are inline — map your data into this array in the module.
170
174
  - \`mode\` — required: \`single\` or \`multi\`.
171
175
  - \`columns?:2|3\` — optional layout column count.
176
+ - \`required?:boolean\` — defaults false. When set, a response selecting no card is refused.
172
177
  - \`label?:string\` — the component's label; required by the multi-response page rule.
173
178
  - \`body?:string\` — optional markdown rendered above the cards. Display-only; it contributes nothing to the response.
174
179
 
@@ -41,6 +41,7 @@ export declare const optionsConfigSchema: z.ZodObject<{
41
41
  single: "single";
42
42
  multi: "multi";
43
43
  }>;
44
+ required: z.ZodOptional<z.ZodBoolean>;
44
45
  allowFreetext: z.ZodOptional<z.ZodBoolean>;
45
46
  freetextLabel: z.ZodOptional<z.ZodString>;
46
47
  freetextPlaceholder: z.ZodOptional<z.ZodString>;
@@ -67,6 +68,7 @@ export declare const textConfigSchema: z.ZodObject<{
67
68
  initialText: z.ZodString;
68
69
  placeholder: z.ZodOptional<z.ZodString>;
69
70
  singleLine: z.ZodOptional<z.ZodBoolean>;
71
+ required: z.ZodOptional<z.ZodBoolean>;
70
72
  }, z.core.$strict>;
71
73
  export type TextConfig = z.infer<typeof textConfigSchema>;
72
74
  export declare const textResponseSchema: z.ZodObject<{
@@ -119,6 +121,7 @@ export declare const tableConfigSchema: z.ZodObject<{
119
121
  single: "single";
120
122
  multi: "multi";
121
123
  }>>;
124
+ required: z.ZodOptional<z.ZodBoolean>;
122
125
  }, z.core.$strict>;
123
126
  export type TableConfig = z.infer<typeof tableConfigSchema>;
124
127
  export declare const tableResponseSchema: z.ZodObject<{
@@ -167,6 +170,7 @@ export declare const cardsConfigSchema: z.ZodObject<{
167
170
  multi: "multi";
168
171
  }>;
169
172
  columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>]>>;
173
+ required: z.ZodOptional<z.ZodBoolean>;
170
174
  }, z.core.$strict>;
171
175
  export type CardsConfig = z.infer<typeof cardsConfigSchema>;
172
176
  export declare const cardsResponseSchema: z.ZodObject<{
@@ -228,6 +232,14 @@ export interface PageManifest {
228
232
  slots: PageSlot[];
229
233
  source?: PageSource;
230
234
  }
235
+ export declare const pageDeliverySchema: z.ZodObject<{
236
+ placement: z.ZodEnum<{
237
+ inline: "inline";
238
+ panel: "panel";
239
+ }>;
240
+ inbox: z.ZodBoolean;
241
+ reply: z.ZodBoolean;
242
+ }, z.core.$strict>;
231
243
  export declare const BUILTIN_PAGE_CONFIG_SCHEMAS: Record<(typeof BUILTIN_PAGE_KINDS)[number], z.ZodType>;
232
244
  export declare const BUILTIN_PAGE_RESPONSE_SCHEMAS: Partial<Record<(typeof BUILTIN_PAGE_KINDS)[number], z.ZodType>>;
233
245
  export declare function issueText(error: z.ZodError): string;
@@ -266,6 +278,8 @@ export declare function validateAuthoredPageManifest(parsed: unknown, productCom
266
278
  export declare function createPageManifestSchema(): z.ZodType<PageManifest>;
267
279
  export declare const pageManifestSchema: z.ZodType<PageManifest, unknown, z.core.$ZodTypeInternals<PageManifest, unknown>>;
268
280
  export type PageResponses = Record<string, SlotResponse>;
281
+ /** A picker's minimum: authored `required` wins, else the mode's own reading of an empty answer. */
282
+ export declare function optionsRequired(config: OptionsConfig): boolean;
269
283
  /** Validate a complete final response map. */
270
284
  export declare function validatePageResponses(manifest: PageManifest, responses: unknown): PageResponses;
271
285
  /** Validate an autosaved response map, permitting omitted response-bearing components. */
@@ -28,6 +28,9 @@ export const optionsConfigSchema = z.object({
28
28
  body: z.string().optional(),
29
29
  options: z.array(optionSchema).min(1).superRefine((items, ctx) => uniqueIds(items, ctx, 'option')),
30
30
  mode: z.enum(['single', 'multi']),
31
+ // Defaults with the mode: a radio group means "pick one", while an empty checkbox
32
+ // list is itself an answer ("none of these apply").
33
+ required: z.boolean().optional(),
31
34
  allowFreetext: z.boolean().optional(),
32
35
  freetextLabel: z.string().optional(),
33
36
  freetextPlaceholder: z.string().optional(),
@@ -40,7 +43,7 @@ export const optionsResponseSchema = z.object({ selectedOptionIds: z.array(itemI
40
43
  // Text is a writing surface, always: its whole point is the string the user hands back, so
41
44
  // there is no read-only mode. `singleLine` only swaps the multi-line surface for one compact
42
45
  // form field — a subject line, a name, a URL.
43
- export const textConfigSchema = z.object({ label: z.string().optional(), initialText: z.string(), placeholder: z.string().optional(), singleLine: z.boolean().optional() }).strict();
46
+ export const textConfigSchema = z.object({ label: z.string().optional(), initialText: z.string(), placeholder: z.string().optional(), singleLine: z.boolean().optional(), required: z.boolean().optional() }).strict();
44
47
  export const textResponseSchema = z.object({ text: z.string(), edited: z.boolean() }).strict();
45
48
  export const tableColumnSchema = z.object({ id: itemIdSchema, label: z.string().min(1), align: z.enum(['left', 'right']).optional(), mono: z.boolean().optional() }).strict();
46
49
  export const tableCellSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
@@ -53,6 +56,7 @@ const tableConfigFields = {
53
56
  rows: tableRowsSchema.optional(),
54
57
  rowSelect: selectModeSchema.default('none'),
55
58
  columnSelect: selectModeSchema.default('none'),
59
+ required: z.boolean().optional(),
56
60
  };
57
61
  /** Table config: its data is always embedded. */
58
62
  export const tableConfigSchema = z.object(tableConfigFields).strict();
@@ -65,6 +69,7 @@ const cardsConfigFields = {
65
69
  cards: cardsSchema.optional(),
66
70
  mode: z.enum(['single', 'multi']),
67
71
  columns: z.union([z.literal(2), z.literal(3)]).optional(),
72
+ required: z.boolean().optional(),
68
73
  };
69
74
  /** Cards config: its data is always embedded. */
70
75
  export const cardsConfigSchema = z.object(cardsConfigFields).strict();
@@ -81,14 +86,14 @@ const chartConfigFields = {
81
86
  /** Chart config: its data is always embedded. */
82
87
  export const chartConfigSchema = z.object(chartConfigFields).strict();
83
88
  const sourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), emittedAt: z.string().datetime({ offset: true }).optional(), profileName: z.string().optional(), nodeId: z.string().optional() }).strict();
84
- const deliverySchema = z.object({ placement: z.enum(['inline', 'panel']), inbox: z.boolean(), reply: z.boolean() }).strict();
89
+ export const pageDeliverySchema = z.object({ placement: z.enum(['inline', 'panel']), inbox: z.boolean(), reply: z.boolean() }).strict();
85
90
  const rawSlotSchema = z.object({ id: idSchema.optional(), kind: z.string().min(1), step: z.number().int().nonnegative(), config: jsonObjectSchema, unvalidated: z.literal(true).optional(), display: z.literal(true).optional() }).strict();
86
91
  const rawPageManifestSchema = z.object({
87
92
  schema: z.literal('crtr.page/v2'),
88
93
  dialect: z.enum(['jsx', 'html']),
89
94
  title: z.string().trim().min(1),
90
95
  subtitle: z.string().optional(),
91
- delivery: deliverySchema,
96
+ delivery: pageDeliverySchema,
92
97
  document: z.enum(['page.tsx', 'page.html']),
93
98
  steps: z.number().int().positive(),
94
99
  slots: z.array(rawSlotSchema),
@@ -254,6 +259,10 @@ function validateComments(slot, comments) {
254
259
  throw new Error(`slot "${slot.id}" does not allow comment anchor kind "${anchor.kind}"`);
255
260
  }
256
261
  }
262
+ /** A picker's minimum: authored `required` wins, else the mode's own reading of an empty answer. */
263
+ export function optionsRequired(config) {
264
+ return config.required ?? config.mode === 'single';
265
+ }
257
266
  function parseResponse(slot, raw) {
258
267
  if (slot.unvalidated === true) {
259
268
  const result = jsonObjectSchema.safeParse(raw);
@@ -276,10 +285,12 @@ function parseResponse(slot, raw) {
276
285
  if (config.mode === 'single' && response.selectedOptionIds.length > 1)
277
286
  throw new Error(`slot "${slot.id}" is single-select but received ${response.selectedOptionIds.length} option ids`);
278
287
  const hasFreetext = config.allowFreetext === true && (response.freetext?.trim() ?? '') !== '';
279
- if (config.mode === 'single' && response.selectedOptionIds.length !== 1 && !hasFreetext)
280
- throw new Error(`single-select slot "${slot.id}" requires exactly one selection or nonempty allowed freetext`);
281
- if (config.mode === 'multi' && response.selectedOptionIds.length === 0 && response.comments.length === 0 && !hasFreetext)
282
- throw new Error(`multi-select slot "${slot.id}" requires a selection, comment, or nonempty allowed freetext`);
288
+ if (optionsRequired(config)) {
289
+ if (config.mode === 'single' && response.selectedOptionIds.length !== 1 && !hasFreetext)
290
+ throw new Error(`single-select slot "${slot.id}" requires exactly one selection or nonempty allowed freetext`);
291
+ if (config.mode === 'multi' && response.selectedOptionIds.length === 0 && response.comments.length === 0 && !hasFreetext)
292
+ throw new Error(`multi-select slot "${slot.id}" requires a selection, comment, or nonempty allowed freetext`);
293
+ }
283
294
  validateComments(slot, response.comments);
284
295
  return response;
285
296
  }
@@ -287,6 +298,8 @@ function parseResponse(slot, raw) {
287
298
  const result = textResponseSchema.safeParse(raw);
288
299
  if (!result.success)
289
300
  throw new Error(`invalid response for slot "${slot.id}": ${issueText(result.error)}`);
301
+ if (slot.config.required === true && result.data.text.trim() === '')
302
+ throw new Error(`slot "${slot.id}" is required and was left empty`);
290
303
  return result.data;
291
304
  }
292
305
  if (slot.kind === 'table') {
@@ -305,6 +318,8 @@ function parseResponse(slot, raw) {
305
318
  for (const id of response.selectedColumnIds)
306
319
  if (!columnIds.has(id))
307
320
  throw new Error(`slot "${slot.id}" selects unknown column id "${id}"`);
321
+ if (config.required === true && response.selectedRowIds.length === 0 && response.selectedColumnIds.length === 0)
322
+ throw new Error(`slot "${slot.id}" is required and nothing is selected`);
308
323
  if ((config.rowSelect === 'none' && response.selectedRowIds.length > 0) || (config.rowSelect === 'single' && response.selectedRowIds.length > 1))
309
324
  throw new Error(`slot "${slot.id}" response violates row select mode "${config.rowSelect}"`);
310
325
  if ((config.columnSelect === 'none' && response.selectedColumnIds.length > 0) || (config.columnSelect === 'single' && response.selectedColumnIds.length > 1))
@@ -324,6 +339,8 @@ function parseResponse(slot, raw) {
324
339
  throw new Error(`slot "${slot.id}" selects unknown card id "${id}"`);
325
340
  if (config.mode === 'single' && response.selectedCardIds.length > 1)
326
341
  throw new Error(`slot "${slot.id}" is single-select but received ${response.selectedCardIds.length} card ids`);
342
+ if (config.required === true && response.selectedCardIds.length === 0)
343
+ throw new Error(`slot "${slot.id}" is required and nothing is selected`);
327
344
  return response;
328
345
  }
329
346
  throw new Error(`slot "${slot.id}" kind "${slot.kind}" does not accept a response`);
@@ -12,3 +12,5 @@ export declare function deriveHtmlPageManifest(document: string, source?: PageSo
12
12
  export declare function derivePageManifest(sourceFile: string, productComponents?: ProductPageComponents, source?: PageSource, delivery?: PageDelivery): PageManifest;
13
13
  /** Read and validate page.json from a ticket directory. */
14
14
  export declare function parsePage(dir: string): PageManifest;
15
+ /** Read only the stable delivery envelope without interpreting component config. */
16
+ export declare function parsePageDelivery(dir: string): PageDelivery;
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { transformSync } from 'esbuild';
4
4
  import ts from 'typescript';
5
5
  import { BUILTIN_KIND_BY_TAG, BUILTIN_PAGE_COMPONENT_TAGS, DISPLAY_PAGE_COMPONENT_TAGS, pageComponentTag } from './page-catalog.js';
6
- import { isResponseBearingSlot, validateAuthoredPageManifest, validatePageManifest } from './page-schema.js';
6
+ import { isResponseBearingSlot, pageDeliverySchema, validateAuthoredPageManifest, validatePageManifest } from './page-schema.js';
7
7
  import { evaluatePageTree, jsonProps } from './page-eval.js';
8
8
  import { PageAuthoringError } from './page-errors.js';
9
9
  export { PageAuthoringError };
@@ -174,14 +174,25 @@ export function deriveHtmlPageManifest(document, source, delivery = { placement:
174
174
  export function derivePageManifest(sourceFile, productComponents = [], source, delivery) {
175
175
  return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), productComponents, source, delivery);
176
176
  }
177
- /** Read and validate page.json from a ticket directory. */
178
- export function parsePage(dir) {
179
- let raw;
177
+ function readPageJson(dir) {
180
178
  try {
181
- raw = JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
179
+ return JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
182
180
  }
183
181
  catch {
184
182
  throw new PageAuthoringError('page.json is not valid JSON');
185
183
  }
186
- return validatePageManifest(raw);
184
+ }
185
+ /** Read and validate page.json from a ticket directory. */
186
+ export function parsePage(dir) {
187
+ return validatePageManifest(readPageJson(dir));
188
+ }
189
+ /** Read only the stable delivery envelope without interpreting component config. */
190
+ export function parsePageDelivery(dir) {
191
+ const raw = readPageJson(dir);
192
+ const parsed = pageDeliverySchema.safeParse(typeof raw === 'object' && raw !== null && !Array.isArray(raw)
193
+ ? raw['delivery']
194
+ : undefined);
195
+ if (!parsed.success)
196
+ throw new PageAuthoringError('page.json has an invalid delivery envelope');
197
+ return parsed.data;
187
198
  }
@@ -3,8 +3,9 @@ import type { PageTicketSummary, TicketSummary } from './types.js';
3
3
  export declare function scanInbox(): TicketSummary[];
4
4
  export type PageHistoryEntry = PageTicketSummary;
5
5
  /**
6
- * Every page ticket raised by one node — pending, resolved or canceled — oldest
7
- * first. This is the durable discovery seam for transcript placement, so it is a
8
- * separate walk from the pending-only inbox scan and never filters on a terminal file.
6
+ * Every page ticket — pending, resolved or canceled — oldest first, optionally
7
+ * narrowed to one raising node. This is the durable discovery seam for
8
+ * transcript placement and for browsing settled tickets, so it is a separate
9
+ * walk from the pending-only inbox scan and never filters on a terminal file.
9
10
  */
10
- export declare function scanPageHistory(nodeId: string): PageHistoryEntry[];
11
+ export declare function scanPageHistory(nodeId?: string): PageHistoryEntry[];
@@ -94,9 +94,10 @@ export function scanInbox() {
94
94
  return items.sort((a, b) => (b.kind === 'page' ? b.emittedAt : b.blockedSince).localeCompare(a.kind === 'page' ? a.emittedAt : a.blockedSince) || a.id.localeCompare(b.id));
95
95
  }
96
96
  /**
97
- * Every page ticket raised by one node — pending, resolved or canceled — oldest
98
- * first. This is the durable discovery seam for transcript placement, so it is a
99
- * separate walk from the pending-only inbox scan and never filters on a terminal file.
97
+ * Every page ticket — pending, resolved or canceled — oldest first, optionally
98
+ * narrowed to one raising node. This is the durable discovery seam for
99
+ * transcript placement and for browsing settled tickets, so it is a separate
100
+ * walk from the pending-only inbox scan and never filters on a terminal file.
100
101
  */
101
102
  export function scanPageHistory(nodeId) {
102
103
  let entries;
@@ -117,7 +118,9 @@ export function scanPageHistory(nodeId) {
117
118
  continue;
118
119
  }
119
120
  const summary = pageSummary(dir, basename(dir));
120
- if (summary === null || summary.source.nodeId !== nodeId)
121
+ if (summary === null)
122
+ continue;
123
+ if (nodeId !== undefined && summary.source.nodeId !== nodeId)
121
124
  continue;
122
125
  const result = summary.state === 'resolved' ? readTicketResult(dir) : null;
123
126
  items.push({ ...summary, ...(result?.kind === 'page' ? { answerDigest: result.summary.replace(/\s+/g, ' ').trim() } : {}) });
@@ -5,7 +5,7 @@ import { z } from 'zod';
5
5
  import { buildPageSummary } from './summary.js';
6
6
  import { atomicWriteJson, atomicWriteText, clearProgress, claimPath, pageManifestPath, pageModulePath, pagePath, progressPath, publishJsonExclusive, requireTicket, responsePath, reviewPath } from './convention.js';
7
7
  import { validateReviewDescriptor, validateReviewProjection } from './review-schema.js';
8
- import { compilePage, deriveHtmlPageManifest, derivePageManifestFromDocument, parsePage, PageAuthoringError } from './page.js';
8
+ import { compilePage, deriveHtmlPageManifest, derivePageManifestFromDocument, parsePage, parsePageDelivery, PageAuthoringError } from './page.js';
9
9
  import { isResponseBearingSlot, validatePageResponses, validatePartialPageResponses } from './page-schema.js';
10
10
  import { signalInboxActivity } from './root.js';
11
11
  import { readTicketClaim, releaseClaimLocked, withTicketLock } from './claim.js';
@@ -145,6 +145,13 @@ function requireSettleablePage(dir) {
145
145
  throw new Error('page_has_no_response_lifecycle');
146
146
  return manifest;
147
147
  }
148
+ function requireCancelablePage(dir) {
149
+ // Cancellation never interprets answers. Reading only this stable envelope
150
+ // keeps an older resident daemon able to retract a page authored by a newer CLI.
151
+ const delivery = parsePageDelivery(dir);
152
+ if (!delivery.inbox && !delivery.reply)
153
+ throw new Error('page_has_no_response_lifecycle');
154
+ }
148
155
  function isEmptyResponseMap(responses) {
149
156
  return typeof responses === 'object' && responses !== null && !Array.isArray(responses) && Object.keys(responses).length === 0;
150
157
  }
@@ -272,7 +279,7 @@ export function cancelTicket(dir, opts = {}) {
272
279
  const ticket = requireTicket(dir);
273
280
  const canceled = withTicketLock(ticket, () => {
274
281
  if (existsSync(pageManifestPath(ticket)))
275
- requireSettleablePage(ticket);
282
+ requireCancelablePage(ticket);
276
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 }) };
277
284
  const won = exclusiveResult(ticket, result);
278
285
  return { status: won ? 'canceled' : 'already_resolved', result: won ? result : readTicketResult(ticket) ?? result };
package/dist/core/io.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import { renameSync, writeFileSync } from 'node:fs';
7
7
  import { CrtrError } from './errors.js';
8
8
  import { ExitCode } from '../types.js';
9
+ import { PREVIEW_RESULT_PATH_ENV } from './preview-result-path.js';
9
10
  import { renderError } from './render.js';
10
11
  import { ApiError } from '../api/index.js';
11
12
  // ---------------------------------------------------------------------------
@@ -16,11 +17,11 @@ let preview;
16
17
  /** Begin collecting the current leaf's structured response, if its caller
17
18
  * supplied the private per-tool-call result path. */
18
19
  export function beginPreview(path) {
19
- const resultPath = process.env['CRTR_PREVIEW_RESULT_PATH'];
20
+ const resultPath = process.env[PREVIEW_RESULT_PATH_ENV];
20
21
  preview = resultPath === undefined || resultPath === '' ? undefined : { path, jsonl: [] };
21
22
  }
22
23
  function publishPreview(record) {
23
- const resultPath = process.env['CRTR_PREVIEW_RESULT_PATH'];
24
+ const resultPath = process.env[PREVIEW_RESULT_PATH_ENV];
24
25
  if (resultPath === undefined || resultPath === '')
25
26
  return;
26
27
  const tempPath = `${resultPath}.${process.pid}.tmp`;
@@ -2,4 +2,6 @@ import type { MarketplaceManifest, PluginManifest } from '../types.js';
2
2
  export declare function pluginManifestPath(pluginRoot: string): string;
3
3
  export declare function marketplaceManifestPath(mktRoot: string): string;
4
4
  export declare function readPluginManifest(pluginRoot: string): PluginManifest | null;
5
+ /** Validate an optional memory-extension declaration at package preflight. */
6
+ export declare function validatePluginManifestMemoryExtensions(manifest: PluginManifest): void;
5
7
  export declare function readMarketplaceManifest(mktRoot: string): MarketplaceManifest | null;
@@ -1,5 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { MARKETPLACE_MANIFEST_DIR, MARKETPLACE_MANIFEST_FILE, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE, } from '../types.js';
3
+ import { validatePluginMemoryExtensions } from './memory/extensions.js';
3
4
  import { readJsonIfExists } from './fs-utils.js';
4
5
  export function pluginManifestPath(pluginRoot) {
5
6
  return join(pluginRoot, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
@@ -10,6 +11,10 @@ export function marketplaceManifestPath(mktRoot) {
10
11
  export function readPluginManifest(pluginRoot) {
11
12
  return readJsonIfExists(pluginManifestPath(pluginRoot));
12
13
  }
14
+ /** Validate an optional memory-extension declaration at package preflight. */
15
+ export function validatePluginManifestMemoryExtensions(manifest) {
16
+ validatePluginMemoryExtensions(manifest);
17
+ }
13
18
  export function readMarketplaceManifest(mktRoot) {
14
19
  return readJsonIfExists(marketplaceManifestPath(mktRoot));
15
20
  }
@@ -0,0 +1,30 @@
1
+ import type { MemoryExtensionDeclarations, MemoryExtensionScalar, PluginManifest } from '../../types.js';
2
+ import type { MemoryDoc } from '../memory-resolver.js';
3
+ export type MemoryExtensionCatalog = Record<string, MemoryExtensionDeclarations>;
4
+ export interface MemoryExtensionValidationIssue {
5
+ path: string;
6
+ message: string;
7
+ }
8
+ export interface MemoryExtensionCatalogOptions {
9
+ /** The manifest currently being staged. Its namespace shadows an installed copy only for that candidate validation. */
10
+ candidate?: Pick<PluginManifest, 'name' | 'memory_extensions'>;
11
+ }
12
+ export declare function isMemoryExtensionScalar(value: unknown): value is MemoryExtensionScalar;
13
+ /** Parse the closed declaration addendum. Callers that accept a plugin manifest
14
+ * invoke this at their install/preflight boundary; normal runtime catalogs only
15
+ * admit declarations which have already cleared that boundary. */
16
+ export declare function validateMemoryExtensionDeclarations(raw: unknown): MemoryExtensionDeclarations;
17
+ /** Throw when a manifest carries an invalid addendum; absent is valid. */
18
+ export declare function validatePluginMemoryExtensions(manifest: Pick<PluginManifest, 'memory_extensions'>): void;
19
+ /** Installed declarations used for validation and authoring. Disabled plugins
20
+ * remain present so existing values can still be checked and edited. */
21
+ export declare function memoryExtensionValidationCatalog(doc: Pick<MemoryDoc, 'scope' | 'path'>, options?: MemoryExtensionCatalogOptions): MemoryExtensionCatalog;
22
+ /** Winning enabled declarations used only to interpret structured output. */
23
+ export declare function memoryExtensionEffectiveCatalog(doc: Pick<MemoryDoc, 'scope' | 'path'>): MemoryExtensionCatalog;
24
+ /** Validate the raw `extensions` frontmatter value. Unresolved namespaces are
25
+ * intentionally reported rather than discarded: removal leaves raw metadata
26
+ * intact but lint-invalid until its plugin returns. */
27
+ export declare function validateMemoryExtensionValues(raw: unknown, catalog: MemoryExtensionCatalog): MemoryExtensionValidationIssue[];
28
+ /** Effective plugin metadata for structured consumers. It overlays only valid
29
+ * explicit scalars onto defaults and never mutates or serializes frontmatter. */
30
+ export declare function projectEffectiveMemoryExtensions(raw: unknown, catalog: MemoryExtensionCatalog): Record<string, Record<string, MemoryExtensionScalar>>;