@north-light/crouter 0.3.191 → 0.3.193

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 +31 -10
  2. package/dist/api/client.js +83 -24
  3. package/dist/api/dto/bash-jobs.d.ts +22 -0
  4. package/dist/api/dto/bash-jobs.js +9 -0
  5. package/dist/api/dto/human.d.ts +3 -4
  6. package/dist/api/dto/inbox.d.ts +106 -65
  7. package/dist/api/dto/inbox.js +2 -8
  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 +7 -0
  12. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
  15. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
  17. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
  18. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  19. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  20. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  21. package/dist/clients/attach/input/overlay-owner.js +1 -1
  22. package/dist/clients/attach/overlays/auth.js +34 -8
  23. package/dist/clients/attach/overlays/pickers.d.ts +5 -8
  24. package/dist/clients/attach/overlays/pickers.js +37 -43
  25. package/dist/clients/attach/viewer.js +643 -620
  26. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
  27. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
  28. package/dist/clients/inbox/controller.d.ts +2 -3
  29. package/dist/clients/inbox/controller.js +41 -42
  30. package/dist/clients/inbox/page-adapter.d.ts +33 -0
  31. package/dist/clients/inbox/page-adapter.js +54 -0
  32. package/dist/clients/inbox/resolve.d.ts +2 -2
  33. package/dist/clients/inbox/resolve.js +1 -1
  34. package/dist/clients/inbox/review-visibility.d.ts +1 -1
  35. package/dist/clients/inbox/review-visibility.js +1 -1
  36. package/dist/clients/inbox/tui/ansi.d.ts +1 -1
  37. package/dist/clients/inbox/tui/ansi.js +2 -2
  38. package/dist/clients/inbox/tui/input.d.ts +1 -3
  39. package/dist/clients/inbox/tui/input.js +242 -553
  40. package/dist/clients/inbox/tui/panel.d.ts +3 -1
  41. package/dist/clients/inbox/tui/panel.js +59 -187
  42. package/dist/clients/inbox/tui/render.d.ts +0 -25
  43. package/dist/clients/inbox/tui/render.js +165 -416
  44. package/dist/clients/inbox/tui/slots.d.ts +32 -0
  45. package/dist/clients/inbox/tui/slots.js +156 -0
  46. package/dist/clients/inbox/tui/types.d.ts +25 -13
  47. package/dist/clients/inbox/tui.js +5 -3
  48. package/dist/commands/attention.js +2 -2
  49. package/dist/commands/human/doc.js +66 -20
  50. package/dist/commands/human/prompts.d.ts +3 -3
  51. package/dist/commands/human/prompts.js +158 -112
  52. package/dist/commands/human/queue.d.ts +0 -1
  53. package/dist/commands/human/queue.js +34 -94
  54. package/dist/commands/human/shared.d.ts +10 -4
  55. package/dist/commands/human/shared.js +62 -13
  56. package/dist/commands/human.js +8 -4
  57. package/dist/commands/memory/read.js +13 -5
  58. package/dist/commands/node/bash.js +15 -28
  59. package/dist/commands/surface/node/focus.js +1 -1
  60. package/dist/core/__tests__/human-deliver.test.js +40 -46
  61. package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
  62. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
  63. package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
  64. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
  65. package/dist/core/auth-interaction.d.ts +2 -2
  66. package/dist/core/bash-jobs.d.ts +32 -0
  67. package/dist/core/bash-jobs.js +72 -1
  68. package/dist/core/canvas/__tests__/attention.test.js +16 -10
  69. package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
  70. package/dist/core/canvas/attention.js +1 -1
  71. package/dist/core/canvas/browse/app.js +1 -1
  72. package/dist/core/canvas/nav-model.d.ts +1 -1
  73. package/dist/core/canvas/nav-model.js +1 -1
  74. package/dist/core/canvas/remote-canvas-source.js +1 -1
  75. package/dist/core/config.js +4 -1
  76. package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
  77. package/dist/core/human/__tests__/page-render.test.js +49 -0
  78. package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
  79. package/dist/core/human/__tests__/page-tickets.test.js +112 -0
  80. package/dist/core/human/__tests__/page.test.d.ts +1 -0
  81. package/dist/core/human/__tests__/page.test.js +139 -0
  82. package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
  83. package/dist/core/human/claim.js +2 -2
  84. package/dist/core/human/convention.d.ts +4 -4
  85. package/dist/core/human/convention.js +6 -13
  86. package/dist/core/human/markdown-html.d.ts +8 -0
  87. package/dist/core/human/markdown-html.js +581 -0
  88. package/dist/core/human/page-catalog.d.ts +6 -0
  89. package/dist/core/human/page-catalog.js +43 -0
  90. package/dist/core/human/page-render.d.ts +3 -0
  91. package/dist/core/human/page-render.js +131 -0
  92. package/dist/core/human/page-schema.d.ts +312 -0
  93. package/dist/core/human/page-schema.js +291 -0
  94. package/dist/core/human/page-synth.d.ts +9 -0
  95. package/dist/core/human/page-synth.js +17 -0
  96. package/dist/core/human/page.d.ts +22 -0
  97. package/dist/core/human/page.js +427 -0
  98. package/dist/core/human/review-schema.d.ts +19 -0
  99. package/dist/core/human/review-schema.js +34 -0
  100. package/dist/core/human/scan.d.ts +12 -3
  101. package/dist/core/human/scan.js +69 -26
  102. package/dist/core/human/summary.d.ts +5 -6
  103. package/dist/core/human/summary.js +54 -37
  104. package/dist/core/human/tickets.d.ts +61 -11
  105. package/dist/core/human/tickets.js +100 -57
  106. package/dist/core/human/types.d.ts +21 -66
  107. package/dist/core/human/types.js +1 -5
  108. package/dist/core/preview-registry.js +0 -1
  109. package/dist/core/review/ticket-filter.d.ts +1 -1
  110. package/dist/core/review/ticket-filter.js +2 -8
  111. package/dist/core/runtime/broker/read-ops.js +3 -0
  112. package/dist/core/runtime/broker.js +19 -10
  113. package/dist/core/runtime/package-health.js +1 -1
  114. package/dist/core/termrender/termrender.d.ts +1 -1
  115. package/dist/core/termrender/termrender.js +2 -2
  116. package/dist/core/tui/page-host.d.ts +1 -1
  117. package/dist/core/tui/page-host.js +2 -2
  118. package/dist/core/user-settings.d.ts +19 -0
  119. package/dist/core/user-settings.js +22 -0
  120. package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
  121. package/dist/daemon/api/handlers/bash-jobs.js +89 -0
  122. package/dist/daemon/api/handlers/human.js +16 -14
  123. package/dist/daemon/api/handlers/inbox.js +311 -227
  124. package/dist/daemon/api/router.d.ts +5 -1
  125. package/dist/daemon/api/router.js +5 -0
  126. package/dist/daemon/api/server.js +2 -0
  127. package/dist/daemon/cron-run.js +23 -10
  128. package/dist/daemon/human/finish.d.ts +8 -3
  129. package/dist/daemon/human/finish.js +35 -15
  130. package/dist/pages/bundle.css +1 -0
  131. package/dist/pages/bundle.js +1347 -0
  132. package/dist/pages/comments.d.ts +34 -0
  133. package/dist/pages/comments.js +72 -0
  134. package/dist/pages/elements/cards.d.ts +17 -0
  135. package/dist/pages/elements/cards.js +785 -0
  136. package/dist/pages/elements/chart.d.ts +1 -0
  137. package/dist/pages/elements/chart.js +648 -0
  138. package/dist/pages/elements/options.d.ts +21 -0
  139. package/dist/pages/elements/options.js +604 -0
  140. package/dist/pages/elements/pages.d.ts +24 -0
  141. package/dist/pages/elements/pages.js +355 -0
  142. package/dist/pages/elements/slot.d.ts +21 -0
  143. package/dist/pages/elements/slot.js +105 -0
  144. package/dist/pages/elements/table.d.ts +19 -0
  145. package/dist/pages/elements/table.js +906 -0
  146. package/dist/pages/elements/text.d.ts +25 -0
  147. package/dist/pages/elements/text.js +951 -0
  148. package/dist/pages/entry.d.ts +8 -0
  149. package/dist/pages/entry.js +8 -0
  150. package/dist/pages/host.d.ts +132 -0
  151. package/dist/pages/host.js +149 -0
  152. package/dist/pages/register.d.ts +2 -0
  153. package/dist/pages/register.js +3 -0
  154. package/dist/pages/slot-config.d.ts +36 -0
  155. package/dist/pages/slot-config.js +50 -0
  156. package/dist/pages/types.d.ts +98 -0
  157. package/dist/pages/types.js +23 -0
  158. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +13 -5
  159. package/dist/pi-extensions/canvas-structured-output.js +7 -3
  160. package/dist/types.d.ts +4 -0
  161. package/dist/types.js +2 -0
  162. package/package.json +8 -8
  163. package/runtime.lock.json +156 -101
  164. package/dist/clients/inbox/deck-adapter.d.ts +0 -29
  165. package/dist/clients/inbox/deck-adapter.js +0 -63
  166. package/dist/core/human/deck-factories.d.ts +0 -8
  167. package/dist/core/human/deck-factories.js +0 -15
  168. package/dist/core/human/deck-schema.d.ts +0 -70
  169. package/dist/core/human/deck-schema.js +0 -92
@@ -722,14 +722,20 @@ async function resolveLaunchModel(services, cfg) {
722
722
  /**
723
723
  * Wrap a live ModelRuntime so a model-less `createAgentSessionFromServices`
724
724
  * call can't have the SDK's own `findInitialModel` silently pick a model
725
- * behind crouter's launch policy. Both `getModel()` (step 3: settingsManager's
726
- * durable `defaultProvider`/`defaultModel` — persisted into a GLOBAL
727
- * settings.json by every `session.setModel()` call the broker itself already
728
- * makes at broker.ts:609/675/1606/1650, so this is a live bypass on any
729
- * machine that has ever picked a model, not a theoretical corner case) and
730
- * `getAvailable()` (step 4: registry default) must both be intercepted —
731
- * wrapping only `getAvailable()` leaves `getModel()`'s step-3 path (and the
732
- * resume path's model restore, sdk.js:88) wide open. Everything else
725
+ * behind crouter's launch policy. Every model-list read the SDK performs during
726
+ * construction must be intercepted:
727
+ * - `getModel()` — findInitialModel step 3 (settingsManager's durable
728
+ * `defaultProvider`/`defaultModel`, persisted into a GLOBAL settings.json by
729
+ * every `session.setModel()` call the broker itself already makes, so this
730
+ * is a live bypass on any machine that has ever picked a model, not a
731
+ * theoretical corner case) and the resume path's model restore.
732
+ * - `getAvailableSnapshot()` — findInitialModel step 4 and
733
+ * restoreModelFromSession's fallback both take the registry default off this
734
+ * SYNCHRONOUS snapshot (model-resolver.js:511/556).
735
+ * - `getAvailable()` — the async refresh-and-list sibling, masked alongside it
736
+ * so no construction-time list read can reach the real registry through
737
+ * either accessor.
738
+ * Masking any one of them alone leaves the others wide open. Everything else
733
739
  * (`getModels`, `hasConfiguredAuth`, `refresh`, …) passes through untouched via
734
740
  * `Reflect.get`. Used ONLY
735
741
  * for the one model-less construction call; the `services` returned to every
@@ -739,7 +745,7 @@ async function resolveLaunchModel(services, cfg) {
739
745
  * CONSTRUCTION-ONLY MASK: the SDK stores `options.services.modelRuntime` as the
740
746
  * session's private `_modelRuntime` for its ENTIRE lifetime and never replaces
741
747
  * it (agent-session-services.js:104-119, sdk.js:253). A permanent mask would
742
- * leave list_models (getAvailable→[]), cycle_model, and provider-rotation
748
+ * leave list_models (the available list→[]), cycle_model, and provider-rotation
743
749
  * fallback (getModel()→undefined) broken forever for any model-less-booted node,
744
750
  * even after a later /login selects a model. Model selection can only happen
745
751
  * DURING construction, so this proxy self-heals: it masks while `masked`, then
@@ -750,7 +756,10 @@ function modelLessRegistry(real) {
750
756
  let masked = true;
751
757
  const runtime = new Proxy(real, {
752
758
  get(target, prop, receiver) {
753
- // `getAvailable` is async on ModelRuntime; findInitialModel awaits it.
759
+ // `getAvailableSnapshot` is the sync list findInitialModel reads; its
760
+ // async sibling `getAvailable` refreshes first and returns the same set.
761
+ if (masked && prop === 'getAvailableSnapshot')
762
+ return () => [];
754
763
  if (masked && prop === 'getAvailable')
755
764
  return async () => [];
756
765
  if (masked && prop === 'getModel')
@@ -23,7 +23,7 @@ import { expandTilde } from '../fs-utils.js';
23
23
  export function rendererWarning() {
24
24
  if (isRendererReady())
25
25
  return null;
26
- return 'crtr: the pinned termrender renderer is not provisioned — decks, reviews, and inline diagrams render as plaintext.\n' +
26
+ return 'crtr: the pinned termrender renderer is not provisioned — pages, reviews, and inline diagrams render as plaintext.\n' +
27
27
  ' Fix: run `crtr sys setup` (requires `uv`: curl -LsSf https://astral.sh/uv/install.sh | sh).';
28
28
  }
29
29
  /**
@@ -41,7 +41,7 @@ export declare function renderMarkdownWithMap(md: string, width: number): Render
41
41
  * The shared block-aware mapped render: prose blocks keep the readability cap
42
42
  * (`proseWidth`), while diagram blocks are re-rendered at the pane's full
43
43
  * `paneWidth` and spliced back in by block index. Both surfaces (terminal
44
- * review and the ask deck) go through this, so "which blocks may be wide" is
44
+ * review and the ask page) go through this, so "which blocks may be wide" is
45
45
  * decided once, from the renderer's own block types, never by heuristics on
46
46
  * the emitted rows.
47
47
  *
@@ -611,7 +611,7 @@ export function renderMarkdownWithMap(md, width) {
611
611
  * The shared block-aware mapped render: prose blocks keep the readability cap
612
612
  * (`proseWidth`), while diagram blocks are re-rendered at the pane's full
613
613
  * `paneWidth` and spliced back in by block index. Both surfaces (terminal
614
- * review and the ask deck) go through this, so "which blocks may be wide" is
614
+ * review and the ask page) go through this, so "which blocks may be wide" is
615
615
  * decided once, from the renderer's own block types, never by heuristics on
616
616
  * the emitted rows.
617
617
  *
@@ -664,7 +664,7 @@ export function renderMarkdownBlockAwareLines(md, proseWidth, paneWidth) {
664
664
  export function checkMarkdown(md) {
665
665
  ensureRenderer();
666
666
  // Renderer unavailable → don't block validation; the body just renders as
667
- // plaintext later. Bricking deck validation here would be the wrong default.
667
+ // plaintext later. Bricking page validation here would be the wrong default.
668
668
  if (rendererState !== 'ready')
669
669
  return { ok: true };
670
670
  const result = spawnSync(VENV_BIN, ['doc', 'check'], {
@@ -1,4 +1,4 @@
1
- import { ProcessTerminal, TUI, type Component, type Focusable } from "@earendil-works/pi-tui";
1
+ import { ProcessTerminal, type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
2
2
  import { readRawScopeConfig } from "../config.js";
3
3
  import { type BindingId, type BindingResolution } from "../keybindings/index.js";
4
4
  /** A full-screen page: a focusable TUI component that cleans up after itself. */
@@ -6,7 +6,7 @@
6
6
  // restore path that survives an exception, a signal, and process exit. Anything
7
7
  // a single page needs beyond that — pi's model runtime, a canvas client — the
8
8
  // page's own runner builds and closes over before calling in here.
9
- import { ProcessTerminal, TUI, } from "@earendil-works/pi-tui";
9
+ import { ProcessTerminal, TuiMainScreen, } from "@earendil-works/pi-tui";
10
10
  import { applyTheme } from "../../clients/attach/config.js";
11
11
  import { readRawScopeConfig } from "../config.js";
12
12
  import { resolveKeybindings, } from "../keybindings/index.js";
@@ -20,7 +20,7 @@ export async function runTuiPage(createSurface, interceptCancel) {
20
20
  const rawUserConfig = readRawScopeConfig("user");
21
21
  const bindings = resolveKeybindings(rawUserConfig?.keybindings);
22
22
  const terminal = new ProcessTerminal();
23
- const tui = new TUI(terminal, true);
23
+ const tui = new TuiMainScreen(terminal, true);
24
24
  return new Promise((resolve, reject) => {
25
25
  let surface;
26
26
  let restored = false;
@@ -196,6 +196,21 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
196
196
  setFromString: (config: ScopeConfig, raw: string) => string | null;
197
197
  };
198
198
  };
199
+ html_pages: {
200
+ surface: "settings";
201
+ setting: {
202
+ key: "html_pages";
203
+ label: string;
204
+ description: string;
205
+ valueConstraint: string;
206
+ applies: string;
207
+ group: "display";
208
+ order: number;
209
+ format: (config: ScopeConfig) => "on" | "off";
210
+ cycle: (config: ScopeConfig) => void;
211
+ setFromString: (config: ScopeConfig, raw: string) => string | null;
212
+ };
213
+ };
199
214
  fold_finished_tools: {
200
215
  surface: "settings";
201
216
  setting: {
@@ -265,6 +280,10 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
265
280
  surface: "specialized";
266
281
  owner: string;
267
282
  };
283
+ page_components: {
284
+ surface: "specialized";
285
+ owner: string;
286
+ };
268
287
  remoteCanvas: {
269
288
  surface: "specialized";
270
289
  owner: string;
@@ -254,6 +254,27 @@ export const CONFIG_SETTINGS_COVERAGE = {
254
254
  },
255
255
  },
256
256
  },
257
+ html_pages: {
258
+ surface: 'settings',
259
+ setting: {
260
+ key: 'html_pages',
261
+ label: 'HTML pages',
262
+ description: 'Teach agent help the HTML page-authoring dialect. Off keeps page authoring Markdown-only.',
263
+ valueConstraint: 'true|false',
264
+ applies: 'Applies on the next help request.',
265
+ group: 'display',
266
+ order: 70,
267
+ format: (config) => config.html_pages ? 'on' : 'off',
268
+ cycle: (config) => { config.html_pages = !config.html_pages; },
269
+ setFromString: (config, raw) => {
270
+ const value = booleanFromString(raw, 'html_pages');
271
+ if (typeof value === 'string')
272
+ return value;
273
+ config.html_pages = value;
274
+ return null;
275
+ },
276
+ },
277
+ },
257
278
  fold_finished_tools: {
258
279
  surface: 'settings',
259
280
  setting: {
@@ -323,6 +344,7 @@ export const CONFIG_SETTINGS_COVERAGE = {
323
344
  modelRoutes: { surface: 'specialized', owner: 'Models route declarations' },
324
345
  modelRouting: { surface: 'specialized', owner: 'Models route declarations' },
325
346
  kinds: { surface: 'specialized', owner: 'Models tab and kind registry' },
347
+ page_components: { surface: 'specialized', owner: 'Page component catalog: product-registered page slot kinds' },
326
348
  remoteCanvas: { surface: 'specialized', owner: 'canvas config commands' },
327
349
  spawnEnv: { surface: 'specialized', owner: 'spawn environment allow-list' },
328
350
  };
@@ -0,0 +1,2 @@
1
+ import type { RouteTable } from '../router.js';
2
+ export declare const bashJobRoutes: RouteTable;
@@ -0,0 +1,89 @@
1
+ // Daemon-owned read and stop surfaces for one node's file-backed background
2
+ // bash jobs. The attach roster continues to read the same directory directly;
3
+ // these routes exist for remote presenters that cannot see the node host.
4
+ import { appendInbox } from '../../../core/feed/inbox.js';
5
+ import { contextDir, isSafeNodeId } from '../../../core/canvas/paths.js';
6
+ import { getNode } from '../../../core/canvas/canvas.js';
7
+ import { isBrokerLive } from '../../../core/runtime/model-swap.js';
8
+ import { assertNotFinalized } from '../../../core/runtime/reopen.js';
9
+ import { reviveNode } from '../../../core/runtime/revive.js';
10
+ import { activeBackgroundBashJobs, bashJobPaths, isBashJobProcessGroupAlive, readJobPgid, isSafeBashJobId as isSafeCoreBashJobId, stopBackgroundBashJob, } from '../../../core/bash-jobs.js';
11
+ import { ApiError } from '../../../api/index.js';
12
+ import { notFound } from '../../../core/errors.js';
13
+ function requireTarget(ctx) {
14
+ const nodeId = ctx.params['id'];
15
+ if (nodeId === undefined || !isSafeNodeId(nodeId)) {
16
+ throw new ApiError(400, 'invalid_node_id', `invalid node id: ${JSON.stringify(nodeId)}`);
17
+ }
18
+ const node = getNode(nodeId);
19
+ if (node === null)
20
+ throw notFound(`unknown node: ${nodeId}`, { received: nodeId });
21
+ const rawJobId = ctx.params['jobId'];
22
+ if (rawJobId !== undefined) {
23
+ if (!isSafeCoreBashJobId(rawJobId)) {
24
+ throw new ApiError(400, 'invalid_job_id', `invalid background job id: ${JSON.stringify(rawJobId)}`);
25
+ }
26
+ return { nodeId, jobId: rawJobId, dir: contextDir(nodeId) };
27
+ }
28
+ return { nodeId, dir: contextDir(nodeId) };
29
+ }
30
+ function handleList(ctx) {
31
+ const { nodeId, dir } = requireTarget(ctx);
32
+ const now = Date.now();
33
+ const body = activeBackgroundBashJobs(dir).map((job) => {
34
+ const pgid = readJobPgid(dir, job.jobId) ?? null;
35
+ return {
36
+ job_id: job.jobId,
37
+ command: job.command,
38
+ started_at: new Date(job.startedAtMs).toISOString(),
39
+ elapsed_ms: Math.max(0, now - job.startedAtMs),
40
+ pgid,
41
+ pgid_alive: pgid !== null && isBashJobProcessGroupAlive(pgid),
42
+ };
43
+ });
44
+ return { status: 200, body };
45
+ }
46
+ async function handleStop(ctx) {
47
+ const { nodeId, jobId, dir } = requireTarget(ctx);
48
+ if (jobId === undefined)
49
+ throw new ApiError(400, 'invalid_request', 'a job id is required');
50
+ const stopped = await stopBackgroundBashJob(dir, jobId);
51
+ if (stopped.kind === 'missing') {
52
+ throw new ApiError(404, 'job_not_found', `no background job ${jobId} for ${nodeId}`);
53
+ }
54
+ // A presenter's roster can be a snapshot; a row whose job has since finished
55
+ // (or was already stopped) is gone, not stoppable.
56
+ if (stopped.kind === 'retired') {
57
+ throw new ApiError(404, 'job_not_found', `background job ${jobId} for ${nodeId} is no longer running`, { log_path: stopped.logPath });
58
+ }
59
+ if (stopped.kind === 'not-stoppable') {
60
+ throw new ApiError(409, 'job_not_stoppable', `job ${jobId} recorded no process group`, { log_path: stopped.logPath });
61
+ }
62
+ // Match `crtr node bash kill`: retire the job, then send an urgent durable
63
+ // notice to the owning node. The terminal path sends that notice through the
64
+ // message endpoint, whose finalized-node gate runs before append; preserve
65
+ // that same fail-loud behavior here after the job control-plane mutation.
66
+ assertNotFinalized(nodeId);
67
+ const paths = bashJobPaths(dir, jobId);
68
+ const body = `Background bash job ${jobId} was stopped by the human before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`;
69
+ appendInbox(nodeId, {
70
+ from: null,
71
+ tier: 'urgent',
72
+ kind: 'message',
73
+ label: body.split('\n')[0].slice(0, 120),
74
+ data: { body },
75
+ });
76
+ const node = getNode(nodeId);
77
+ if (node !== null && !isBrokerLive(node)) {
78
+ try {
79
+ reviveNode(nodeId, { resume: true });
80
+ }
81
+ catch { /* the notice remains durable */ }
82
+ }
83
+ const result = { node_id: nodeId, job_id: jobId, signaled: stopped.signaled };
84
+ return { status: 200, body: result };
85
+ }
86
+ export const bashJobRoutes = [
87
+ { method: 'GET', pattern: '/v1/nodes/:id/jobs', handler: handleList },
88
+ { method: 'DELETE', pattern: '/v1/nodes/:id/jobs/:jobId', handler: handleStop },
89
+ ];
@@ -4,13 +4,14 @@
4
4
  import { existsSync } from 'node:fs';
5
5
  import { ApiError } from '../../../api/index.js';
6
6
  import { isSafeNodeId } from '../../../core/canvas/paths.js';
7
- import { deckPath, reviewPath } from '../../../core/human/convention.js';
7
+ import { pageManifestPath, reviewPath } from '../../../core/human/convention.js';
8
8
  import { ticketDir } from '../../../core/human/root.js';
9
- import { readTicketResult, validateDeckTicketResponses } from '../../../core/human/tickets.js';
9
+ import { readTicketResult, validatePageTicketResponses } from '../../../core/human/tickets.js';
10
+ import { readConfig } from '../../../core/config.js';
10
11
  import { getReviewByBridge } from '../../../core/review/store.js';
11
12
  import { ReviewOperationError } from '../../../core/review/types.js';
12
13
  import { spawnNode } from '../../../core/runtime/nodes.js';
13
- import { cancelHumanTicket, resolveDeckTicket } from '../../human/finish.js';
14
+ import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
14
15
  import { cancelReview } from '../../review/finish.js';
15
16
  function requireTicketNodeId(ctx) {
16
17
  const nodeId = ctx.params['node_id'];
@@ -52,7 +53,7 @@ function handleCreateBridge(ctx) {
52
53
  function reviewSubmitGuidance() {
53
54
  return new ApiError(400, 'invalid_request', 'review approvals use POST /v1/human/reviews/:review_id/submit');
54
55
  }
55
- /** The ticket resolve route accepts only a deck response body. */
56
+ /** The ticket resolve route accepts only a page response body. */
56
57
  function requireResolveRequest(ctx) {
57
58
  const request = ctx.body;
58
59
  if (request === undefined || request === null || typeof request !== 'object' || Array.isArray(request)) {
@@ -66,13 +67,13 @@ function requireResolveRequest(ctx) {
66
67
  throw reviewSubmitGuidance();
67
68
  throw new ApiError(400, 'invalid_request', 'ticket resolve body requires exactly `responses`');
68
69
  }
69
- function requireDeckTicket(dir) {
70
- const deck = existsSync(deckPath(dir));
70
+ function requirePageTicket(dir) {
71
+ const page = existsSync(pageManifestPath(dir));
71
72
  const review = existsSync(reviewPath(dir));
72
- if (review && !deck)
73
+ if (review && !page)
73
74
  throw reviewSubmitGuidance();
74
- if (!deck || review) {
75
- throw new ApiError(400, 'invalid_request', 'ticket has no unambiguous deck kind');
75
+ if (!page || review) {
76
+ throw new ApiError(400, 'invalid_request', 'ticket has no unambiguous page kind');
76
77
  }
77
78
  }
78
79
  function invalidResolveRequest(error) {
@@ -91,13 +92,14 @@ async function handleResolve(ctx) {
91
92
  const nodeId = requireTicketNodeId(ctx);
92
93
  const request = requireResolveRequest(ctx);
93
94
  const dir = ticketDir(nodeId);
94
- requireDeckTicket(dir);
95
- const responses = preflightResolve(() => validateDeckTicketResponses(dir, request.responses));
96
- await resolveDeckTicket(nodeId, responses);
97
- return { status: 200, body: { delivered: true, kind: 'deck' } };
95
+ requirePageTicket(dir);
96
+ const productKinds = readConfig('user').page_components ?? [];
97
+ const responses = preflightResolve(() => validatePageTicketResponses(dir, request.responses, productKinds));
98
+ await resolvePageTicket(nodeId, responses, productKinds);
99
+ return { status: 200, body: { delivered: true, kind: 'page' } };
98
100
  }
99
101
  function hasPublishedTicket(dir) {
100
- return existsSync(deckPath(dir)) || existsSync(reviewPath(dir)) || readTicketResult(dir) !== null;
102
+ return existsSync(pageManifestPath(dir)) || existsSync(reviewPath(dir)) || readTicketResult(dir) !== null;
101
103
  }
102
104
  async function handleCancel(ctx) {
103
105
  const nodeId = requireTicketNodeId(ctx);