@north-light/crouter 0.3.197 → 0.3.198

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 (144) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/dto/inbox.d.ts +46 -0
  3. package/dist/builtin-memory/00-runtime-base.md +3 -3
  4. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  5. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  7. package/dist/clients/attach/render/chat-view.d.ts +3 -8
  8. package/dist/clients/attach/render/chat-view.js +3 -24
  9. package/dist/clients/attach/render/markdown-source.js +0 -6
  10. package/dist/clients/attach/render/tool-calls.js +16 -2
  11. package/dist/clients/attach/viewer.js +613 -602
  12. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
  13. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
  14. package/dist/clients/inbox/controller.js +4 -4
  15. package/dist/clients/inbox/page-adapter.d.ts +2 -1
  16. package/dist/clients/inbox/tui/render.js +46 -25
  17. package/dist/clients/inbox/tui/slots.d.ts +1 -3
  18. package/dist/clients/inbox/tui/slots.js +8 -37
  19. package/dist/clients/inbox/tui/types.d.ts +4 -3
  20. package/dist/commands/__tests__/human.test.js +160 -23
  21. package/dist/commands/canvas-browse.js +1 -1
  22. package/dist/commands/human/doc.js +21 -74
  23. package/dist/commands/human/html.d.ts +2 -0
  24. package/dist/commands/human/html.js +81 -0
  25. package/dist/commands/human/prompts.d.ts +5 -4
  26. package/dist/commands/human/prompts.js +81 -153
  27. package/dist/commands/human/shared.d.ts +5 -6
  28. package/dist/commands/human/shared.js +13 -61
  29. package/dist/commands/human.js +7 -2
  30. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  31. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  32. package/dist/core/__tests__/human-deliver.test.js +28 -37
  33. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  34. package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
  35. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
  36. package/dist/core/canvas/__tests__/attention.test.js +2 -2
  37. package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
  38. package/dist/core/canvas/browse/app.d.ts +1 -1
  39. package/dist/core/canvas/browse/app.js +21 -3
  40. package/dist/core/canvas/browse/render.d.ts +4 -0
  41. package/dist/core/canvas/browse/render.js +5 -1
  42. package/dist/core/canvas/canvas.d.ts +26 -0
  43. package/dist/core/canvas/canvas.js +55 -0
  44. package/dist/core/command.js +6 -11
  45. package/dist/core/config.js +3 -2
  46. package/dist/core/help.d.ts +3 -0
  47. package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
  48. package/dist/core/human/__tests__/html-markdown.test.js +52 -0
  49. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  50. package/dist/core/human/__tests__/page-catalog.test.js +16 -0
  51. package/dist/core/human/__tests__/page-render.test.js +40 -23
  52. package/dist/core/human/__tests__/page-tickets.test.js +48 -27
  53. package/dist/core/human/__tests__/page.test.js +48 -65
  54. package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
  55. package/dist/core/human/answer-text.d.ts +3 -0
  56. package/dist/core/human/answer-text.js +98 -0
  57. package/dist/core/human/answer.d.ts +51 -0
  58. package/dist/core/human/answer.js +125 -0
  59. package/dist/core/human/component-docs.d.ts +14 -0
  60. package/dist/core/human/component-docs.js +161 -0
  61. package/dist/core/human/html-markdown.d.ts +3 -0
  62. package/dist/core/human/html-markdown.js +288 -0
  63. package/dist/core/human/page-catalog.d.ts +19 -3
  64. package/dist/core/human/page-catalog.js +73 -16
  65. package/dist/core/human/page-render.d.ts +3 -2
  66. package/dist/core/human/page-render.js +19 -45
  67. package/dist/core/human/page-schema.d.ts +108 -8
  68. package/dist/core/human/page-schema.js +88 -31
  69. package/dist/core/human/page-synth.d.ts +2 -4
  70. package/dist/core/human/page-synth.js +23 -7
  71. package/dist/core/human/page.d.ts +14 -7
  72. package/dist/core/human/page.js +178 -90
  73. package/dist/core/human/review-schema.d.ts +1 -0
  74. package/dist/core/human/review-schema.js +1 -1
  75. package/dist/core/human/scan.d.ts +3 -2
  76. package/dist/core/human/scan.js +7 -8
  77. package/dist/core/human/summary.d.ts +4 -7
  78. package/dist/core/human/summary.js +4 -60
  79. package/dist/core/human/tickets.d.ts +10 -9
  80. package/dist/core/human/tickets.js +5 -3
  81. package/dist/core/human/types.d.ts +4 -0
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +1 -0
  84. package/dist/core/review/realize.js +9 -1
  85. package/dist/core/runtime/boot-root.js +6 -1
  86. package/dist/core/runtime/broker.js +8 -6
  87. package/dist/core/runtime/busy.d.ts +4 -3
  88. package/dist/core/runtime/busy.js +4 -3
  89. package/dist/core/runtime/spawn.js +4 -4
  90. package/dist/core/user-settings.d.ts +17 -2
  91. package/dist/core/user-settings.js +32 -11
  92. package/dist/daemon/api/handlers/human.js +1 -1
  93. package/dist/daemon/api/handlers/inbox.js +22 -7
  94. package/dist/daemon/cron-run.js +16 -2
  95. package/dist/daemon/crtrd.js +1 -1
  96. package/dist/daemon/fleet.d.ts +22 -10
  97. package/dist/daemon/fleet.js +43 -24
  98. package/dist/daemon/human/finish.d.ts +2 -1
  99. package/dist/daemon/human/finish.js +45 -28
  100. package/dist/daemon/human/sweep.js +4 -2
  101. package/dist/daemon/reconcilers/broker-supervision.js +11 -8
  102. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  103. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  104. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  105. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  106. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  107. package/dist/daemon/review/sweep.js +12 -1
  108. package/dist/pages/bundle.css +1 -1
  109. package/dist/pages/bundle.js +1008 -577
  110. package/dist/pages/comments.d.ts +29 -0
  111. package/dist/pages/comments.js +51 -0
  112. package/dist/pages/controls.d.ts +98 -0
  113. package/dist/pages/controls.js +261 -0
  114. package/dist/pages/elements/cards.d.ts +15 -4
  115. package/dist/pages/elements/cards.js +299 -186
  116. package/dist/pages/elements/chart.d.ts +22 -0
  117. package/dist/pages/elements/chart.js +107 -91
  118. package/dist/pages/elements/options.d.ts +9 -0
  119. package/dist/pages/elements/options.js +268 -155
  120. package/dist/pages/elements/pages.d.ts +6 -0
  121. package/dist/pages/elements/pages.js +60 -38
  122. package/dist/pages/elements/slot.d.ts +5 -0
  123. package/dist/pages/elements/slot.js +17 -10
  124. package/dist/pages/elements/table.d.ts +17 -0
  125. package/dist/pages/elements/table.js +336 -228
  126. package/dist/pages/elements/text.d.ts +9 -3
  127. package/dist/pages/elements/text.js +350 -143
  128. package/dist/pages/host.d.ts +0 -24
  129. package/dist/pages/host.js +0 -17
  130. package/dist/pages/readonly.d.ts +24 -0
  131. package/dist/pages/readonly.js +31 -0
  132. package/dist/pages/responses.d.ts +37 -0
  133. package/dist/pages/responses.js +56 -0
  134. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  135. package/dist/pi-extensions/canvas-bash-valve.js +12 -8
  136. package/dist/types.d.ts +19 -5
  137. package/dist/types.js +2 -1
  138. package/package.json +1 -1
  139. package/runtime.lock.json +2 -2
  140. package/scripts/install-runtime.mjs +19 -24
  141. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  142. package/dist/clients/attach/render/html-markdown.js +0 -206
  143. package/dist/core/human/markdown-html.d.ts +0 -8
  144. package/dist/core/human/markdown-html.js +0 -581
@@ -5,7 +5,7 @@ import { z } from 'zod';
5
5
  import { buildPageSummary } from './summary.js';
6
6
  import { atomicWriteJson, clearProgress, claimPath, pageManifestPath, pagePath, progressPath, publishJsonExclusive, requireTicket, responsePath, reviewPath } from './convention.js';
7
7
  import { validateReviewDescriptor, validateReviewProjection } from './review-schema.js';
8
- import { derivePageManifestFromDocument, parsePage, PageAuthoringError } from './page.js';
8
+ import { derivePageManifestFromDocument, parsePage, PageAuthoringError, snapshotPageSources } from './page.js';
9
9
  import { validatePageResponses, validatePartialPageResponses } from './page-schema.js';
10
10
  import { signalInboxActivity } from './root.js';
11
11
  import { readTicketClaim, releaseClaimLocked, withTicketLock } from './claim.js';
@@ -62,7 +62,9 @@ export function preparePage(opts) {
62
62
  throw new PageAuthoringError(`page source file does not exist: ${opts.sourceFile}`);
63
63
  const document = readFileSync(opts.sourceFile, 'utf8');
64
64
  const stamped = { ...(opts.source ?? {}), blockedSince: opts.source?.blockedSince ?? new Date().toISOString() };
65
- return { dialect: opts.dialect, document, manifest: derivePageManifestFromDocument(document, opts.dialect, opts.productKinds ?? [], stamped) };
65
+ const authored = derivePageManifestFromDocument(document, opts.dialect, opts.productKinds ?? [], stamped);
66
+ const manifest = snapshotPageSources(authored, opts.productKinds ?? []);
67
+ return { dialect: opts.dialect, document, manifest };
66
68
  }
67
69
  /** Publish one prepared page ticket: the authored source plus its manifest, exclusively. */
68
70
  export function publishPage(dir, prepared) {
@@ -170,7 +172,7 @@ function completePageLocked(dir, responses, claimToken, opts) {
170
172
  requireClaimOwnership(dir, claimToken);
171
173
  const manifest = requirePage(dir, opts.productKinds ?? []);
172
174
  const parsedResponses = validatePageResponses(manifest, responses);
173
- const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses), completedAt: opts.completedAt ?? new Date().toISOString() };
175
+ const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses, opts.productKinds), completedAt: opts.completedAt ?? new Date().toISOString() };
174
176
  const won = exclusiveResult(dir, result);
175
177
  clearOwnedWork(dir, claimToken);
176
178
  return { won, result: won ? result : readTicketResult(dir) ?? result };
@@ -4,6 +4,8 @@ export interface TicketSource {
4
4
  sessionName?: string;
5
5
  askedBy?: string;
6
6
  blockedSince?: string;
7
+ /** Profile name the originating node runs under. Product consumers use this stable name to resolve the applet or worker identity behind the ticket. */
8
+ profileName?: string;
7
9
  /** Originating canvas node id (CRTR_NODE_ID) when the ask was raised inside a crouter canvas node. */
8
10
  nodeId?: string;
9
11
  }
@@ -48,6 +50,8 @@ export interface ReviewTicketSummary extends TicketSummaryBase {
48
50
  /** Page triage fields: a client picks a transcript rendering without a second fetch. */
49
51
  export interface PageTicketSummary extends TicketSummaryBase {
50
52
  kind: 'page';
53
+ /** Present on resolved history entries; the stored one-line response digest. */
54
+ answerDigest?: string;
51
55
  dialect: 'md' | 'html';
52
56
  placement: 'inline' | 'panel';
53
57
  steps: number;
@@ -3,13 +3,13 @@ import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
3
3
  type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
4
4
  type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
5
5
  type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
6
- type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus' | 'focus-background';
6
+ type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
7
7
  type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin';
8
8
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
9
9
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
10
10
  type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
11
11
  export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
12
- export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
12
+ export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
13
13
  export declare const BINDING_IDS: ReadonlySet<string>;
14
14
  /** True for an action whose direct gesture is handled inside an attach viewer.
15
15
  * Tmux menu selectors and root shortcuts are chrome, not pane input. */
@@ -142,6 +142,7 @@ function browseBindings() {
142
142
  b('crtr.browse.revive-now', 'Browse and pickers', 'Revive now', 'Revive the selected node now.', ['shift+k'], 'terminal', [nav]),
143
143
  b('crtr.browse.focus', 'Browse and pickers', 'Focus node', 'Focus the selected node.', ['enter', 'o'], 'terminal', [nav]),
144
144
  b('crtr.browse.focus-background', 'Browse and pickers', 'Focus node (keep browsing)', 'Focus the selected node without leaving the navigator.', ['shift+o'], 'terminal', [nav]),
145
+ b('crtr.browse.yank-id', 'Browse and pickers', 'Yank node id', 'Copy the selected node id to the system clipboard.', ['y'], 'terminal', [nav]),
145
146
  b('crtr.browse.search.cancel', 'Browse and pickers', 'Cancel search', 'Cancel browse search.', ['escape'], 'terminal', ['browse.search'], 'dismiss-modal'),
146
147
  b('crtr.browse.search.commit', 'Browse and pickers', 'Commit search', 'Apply browse search.', ['enter'], 'terminal', ['browse.search']),
147
148
  b('crtr.browse.search.delete', 'Browse and pickers', 'Delete search text', 'Delete the previous search character.', ['backspace'], 'terminal', ['browse.search']),
@@ -6,6 +6,7 @@ import { atomicWriteJson } from '../human/convention.js';
6
6
  import { ticketDir } from '../human/root.js';
7
7
  import { submitReview } from '../human/tickets.js';
8
8
  import { NodeIdConflictError, spawnNode } from '../runtime/nodes.js';
9
+ import { loadProfileManifest } from '../profiles/manifest.js';
9
10
  import { transition } from '../runtime/lifecycle.js';
10
11
  import { reviveNode } from '../runtime/revive.js';
11
12
  import { awaitCompanionBind, spawnReviewCompanion } from './companion.js';
@@ -126,6 +127,10 @@ function publishTicket(review) {
126
127
  throw bridgeUnsubscribed(review, '');
127
128
  if (review.subtitle === null)
128
129
  throw new Error(`ticket review has no subtitle: ${review.review_id}`);
130
+ const origin = requireOrigin(review);
131
+ const profileName = origin.profile_id == null
132
+ ? undefined
133
+ : loadProfileManifest(origin.profile_id).manifest.name;
129
134
  submitReview({
130
135
  dir: ticketDir(review.bridge_node_id),
131
136
  review: {
@@ -133,7 +138,10 @@ function publishTicket(review) {
133
138
  ...(review.output_path !== null ? { output: review.output_path } : {}),
134
139
  title: basename(review.file),
135
140
  subtitle: review.subtitle,
136
- source: { nodeId: review.origin_node_id },
141
+ source: {
142
+ nodeId: review.origin_node_id,
143
+ ...(profileName === undefined ? {} : { profileName }),
144
+ },
137
145
  blockedSince: review.created,
138
146
  },
139
147
  });
@@ -89,7 +89,12 @@ export async function bootRoot(opts) {
89
89
  // user's OWN pre-existing shell pane (not one crtr split open): on a
90
90
  // detach-reap of an empty root it must delete the node WITHOUT kill-pane, else
91
91
  // a plain ctrl+c closes the user's whole tmux pane.
92
- const attachEnv = { ...process.env, ...viewerSplitEnv(), CRTR_ATTACH_WAIT_MS: '30000' };
92
+ const attachEnv = {
93
+ ...process.env,
94
+ ...viewerSplitEnv(),
95
+ CRTR_ATTACH_WAIT_MS: '30000',
96
+ ...(opts.prompt !== undefined && opts.prompt !== '' ? { CRTR_ATTACH_INITIAL_WHIP: '1' } : {}),
97
+ };
93
98
  if (here !== null)
94
99
  attachEnv['CRTR_ATTACH_BORROWED_PANE'] = here.pane;
95
100
  mark('boot_root.viewer_spawn');
@@ -120,7 +120,8 @@ export function disposeActiveSession() {
120
120
  // ---------------------------------------------------------------------------
121
121
  export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), startupTs = new Date().toISOString()) {
122
122
  // A clean-abort certificate belongs only to the dead broker that wrote it.
123
- // Fleet classification consumes it before launching this successor.
123
+ // Fleet classification consumes it as proof that the dead broker had a turn
124
+ // in flight even though Pi's settled abort already cleared the busy marker.
124
125
  clearCleanAbort(nodeId);
125
126
  // The canonical broker event source (job/log.jsonl) is bound before any other
126
127
  // work in this function — no path/env/state setup may precede it, and the
@@ -594,9 +595,10 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
594
595
  // A daemon fleet bounce gives a busy Pi a short, bounded chance to persist
595
596
  // its aborted turn before teardown. `session.abort()` resolves only after Pi
596
597
  // reaches idle (and thus after its message_end persistence); only then may we
597
- // leave the durable clean-abort certificate that changes recovery to resume.
598
- // A timeout/rejection deliberately leaves no certificate, preserving dirty
599
- // crash → recovery-cycle behavior and the fleet's shared SIGKILL deadline.
598
+ // leave the durable clean-abort certificate that preserves the otherwise
599
+ // cleared evidence that this broker had a turn in flight. A timeout/rejection
600
+ // deliberately leaves no certificate: the surviving busy marker carries the
601
+ // same interrupted-turn fact through the fleet's shared SIGKILL deadline.
600
602
  const ABORT_BEFORE_EXIT_MS = 2_500;
601
603
  let signalExitInFlight = false;
602
604
  const abortAndExit = async (reason) => {
@@ -633,8 +635,8 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
633
635
  }
634
636
  else if (frameDispatch.pendingPromptDispatches() > 0 || rebind.isAgentRunActive()) {
635
637
  // The engine never reached an abortable persisted turn in the budget.
636
- // Preserve the existing dirty-death recovery cycle rather than certifying
637
- // a session that may not contain the prompt it was preparing.
638
+ // Preserve the dirty interrupted-turn witness so recovery resumes the
639
+ // saved transcript and explicitly re-drives the stopped work.
638
640
  markBusy(nodeId);
639
641
  }
640
642
  }
@@ -4,12 +4,13 @@ export declare function markBusy(nodeId: string): void;
4
4
  /** Clear the mid-run marker after Pi settles, however it routed. Best-effort. */
5
5
  export declare function clearBusy(nodeId: string): void;
6
6
  /** Certify that this broker aborted a busy turn and Pi persisted its settled
7
- * aborted entry before teardown. The daemon consumes this durable certificate
8
- * once to resume with a continuation; a dirty death never writes it. */
7
+ * aborted entry before teardown. Settling clears the busy marker, so the daemon
8
+ * consumes this durable certificate as the surviving interrupted-turn witness
9
+ * and resumes with a continuation. A dirty death instead leaves `busy` behind. */
9
10
  export declare function markCleanAbort(nodeId: string): void;
10
11
  /** Remove a consumed clean-abort certificate as a broker boots. */
11
12
  export declare function clearCleanAbort(nodeId: string): void;
12
- /** Whether the dead broker left a persisted clean-abort certificate. */
13
+ /** Whether the dead broker left a persisted clean-abort turn witness. */
13
14
  export declare function hasCleanAbort(nodeId: string): boolean;
14
15
  /** Is the node currently inside a turn? AND this with `pidAlive` at the call
15
16
  * site — a stale marker from a crashed pi is harmless because the dead pid
@@ -44,8 +44,9 @@ export function clearBusy(nodeId) {
44
44
  }
45
45
  }
46
46
  /** Certify that this broker aborted a busy turn and Pi persisted its settled
47
- * aborted entry before teardown. The daemon consumes this durable certificate
48
- * once to resume with a continuation; a dirty death never writes it. */
47
+ * aborted entry before teardown. Settling clears the busy marker, so the daemon
48
+ * consumes this durable certificate as the surviving interrupted-turn witness
49
+ * and resumes with a continuation. A dirty death instead leaves `busy` behind. */
49
50
  export function markCleanAbort(nodeId) {
50
51
  try {
51
52
  mkdirSync(jobDir(nodeId), { recursive: true });
@@ -64,7 +65,7 @@ export function clearCleanAbort(nodeId) {
64
65
  /* best-effort */
65
66
  }
66
67
  }
67
- /** Whether the dead broker left a persisted clean-abort certificate. */
68
+ /** Whether the dead broker left a persisted clean-abort turn witness. */
68
69
  export function hasCleanAbort(nodeId) {
69
70
  return existsSync(cleanAbortPath(nodeId));
70
71
  }
@@ -401,15 +401,15 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
401
401
  // have bound view.sock yet. Hand the attach pane CRTR_ATTACH_WAIT_MS so
402
402
  // it self-polls the socket until it accepts instead of connecting once,
403
403
  // finding a cold socket, and closing the pane instantly — the exact
404
- // mechanism bootRoot uses. Mark this as the initial auto-opened child
405
- // viewer too, so whip_mode can greet it without firing on ordinary
406
- // focus/attach paths. No pre-wait in this CLI subprocess.
404
+ // mechanism bootRoot uses. A child with a kickoff prompt marks its
405
+ // initial viewer for whip_mode; ordinary focus/attach paths stay
406
+ // silent. No pre-wait in this CLI subprocess.
407
407
  const viewer = openViewerWindow(meta.node_id, parentViewer.session ?? '', {
408
408
  name: fullName(meta),
409
409
  cwd: opts.cwd,
410
410
  besidePane: parentViewer.pane,
411
411
  waitForBroker: true,
412
- initialWhip: true,
412
+ initialWhip: hasKickoff,
413
413
  });
414
414
  if (viewer !== null && viewer.pane !== null && viewer.pane !== '') {
415
415
  session = viewer.session;
@@ -147,6 +147,21 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
147
147
  setFromString: (config: ScopeConfig, raw: string) => string | null;
148
148
  };
149
149
  };
150
+ html_surface: {
151
+ surface: "settings";
152
+ setting: {
153
+ key: "html_surface";
154
+ label: string;
155
+ description: string;
156
+ valueConstraint: string;
157
+ applies: string;
158
+ group: "display";
159
+ order: number;
160
+ format: (config: ScopeConfig) => "on" | "off";
161
+ cycle: (config: ScopeConfig) => void;
162
+ setFromString: (config: ScopeConfig, raw: string) => string | null;
163
+ };
164
+ };
150
165
  whip_messages: {
151
166
  surface: "specialized";
152
167
  owner: string;
@@ -196,10 +211,10 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
196
211
  setFromString: (config: ScopeConfig, raw: string) => string | null;
197
212
  };
198
213
  };
199
- html_pages: {
214
+ bash_tool_purpose: {
200
215
  surface: "settings";
201
216
  setting: {
202
- key: "html_pages";
217
+ key: "bash_tool_purpose";
203
218
  label: string;
204
219
  description: string;
205
220
  valueConstraint: string;
@@ -176,7 +176,7 @@ export const CONFIG_SETTINGS_COVERAGE = {
176
176
  setting: {
177
177
  key: 'whip_mode',
178
178
  label: 'Whip mode',
179
- description: 'Play the whip animation when you send a prompt and when an automatically opened child viewer starts.',
179
+ description: 'Play the whip animation when you send a prompt and when a viewer opens for an agent with an initial prompt.',
180
180
  valueConstraint: 'true|false',
181
181
  applies: 'Applies on the next attach.',
182
182
  group: 'display',
@@ -192,6 +192,27 @@ export const CONFIG_SETTINGS_COVERAGE = {
192
192
  },
193
193
  },
194
194
  },
195
+ html_surface: {
196
+ surface: 'settings',
197
+ setting: {
198
+ key: 'html_surface',
199
+ label: 'HTML human surface',
200
+ description: 'Prompt agents to use HTML pages by default for structured or interactive content shown to you.',
201
+ valueConstraint: 'true|false',
202
+ applies: 'Applies when human command help is assembled.',
203
+ group: 'display',
204
+ order: 37,
205
+ format: (config) => config.html_surface ? 'on' : 'off',
206
+ cycle: (config) => { config.html_surface = !config.html_surface; },
207
+ setFromString: (config, raw) => {
208
+ const value = booleanFromString(raw, 'html_surface');
209
+ if (typeof value === 'string')
210
+ return value;
211
+ config.html_surface = value;
212
+ return null;
213
+ },
214
+ },
215
+ },
195
216
  whip_messages: { surface: 'specialized', owner: 'sys config set whip_messages' },
196
217
  mouse_mode_default: {
197
218
  surface: 'settings',
@@ -254,23 +275,23 @@ export const CONFIG_SETTINGS_COVERAGE = {
254
275
  },
255
276
  },
256
277
  },
257
- html_pages: {
278
+ bash_tool_purpose: {
258
279
  surface: 'settings',
259
280
  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.',
281
+ key: 'bash_tool_purpose',
282
+ label: 'Bash purpose field',
283
+ description: 'Expose the optional purpose label to agents in the bash tool schema. Off removes the field without disabling the bash valve.',
263
284
  valueConstraint: 'true|false',
264
- applies: 'Applies on the next help request.',
285
+ applies: 'Applies on the next node session.',
265
286
  group: 'display',
266
- order: 70,
267
- format: (config) => config.html_pages ? 'on' : 'off',
268
- cycle: (config) => { config.html_pages = !config.html_pages; },
287
+ order: 25,
288
+ format: (config) => config.bash_tool_purpose ? 'on' : 'off',
289
+ cycle: (config) => { config.bash_tool_purpose = !config.bash_tool_purpose; },
269
290
  setFromString: (config, raw) => {
270
- const value = booleanFromString(raw, 'html_pages');
291
+ const value = booleanFromString(raw, 'bash_tool_purpose');
271
292
  if (typeof value === 'string')
272
293
  return value;
273
- config.html_pages = value;
294
+ config.bash_tool_purpose = value;
274
295
  return null;
275
296
  },
276
297
  },
@@ -93,7 +93,7 @@ async function handleResolve(ctx) {
93
93
  const request = requireResolveRequest(ctx);
94
94
  const dir = ticketDir(nodeId);
95
95
  requirePageTicket(dir);
96
- const productKinds = readConfig('user').page_components ?? [];
96
+ const productKinds = readConfig('user').page_components;
97
97
  const responses = preflightResolve(() => validatePageTicketResponses(dir, request.responses, productKinds));
98
98
  await resolvePageTicket(nodeId, responses, productKinds);
99
99
  return { status: 200, body: { delivered: true, kind: 'page' } };
@@ -7,10 +7,12 @@ import { basename, resolve, join } from 'node:path';
7
7
  import { ApiError } from '../../../api/index.js';
8
8
  import { assetRoot } from '../../../core/asset-root.js';
9
9
  import { pageManifestPath, reviewPath } from '../../../core/human/convention.js';
10
+ import { parsePage } from '../../../core/human/page.js';
10
11
  import { renderPageHtml } from '../../../core/human/page-render.js';
11
12
  import { ticketsRoot } from '../../../core/human/root.js';
12
13
  import { scanInbox, scanPageHistory } from '../../../core/human/scan.js';
13
14
  import { readPageTicket, readTicketResult, validatePageTicketResponses, writePageProgress, } from '../../../core/human/tickets.js';
15
+ import { describePageAnswer } from '../../../core/human/answer.js';
14
16
  import { readConfig } from '../../../core/config.js';
15
17
  import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
16
18
  import { filterTerminalReviewTickets } from '../../../core/review/ticket-filter.js';
@@ -80,6 +82,8 @@ function pageSlotToDTO(slot) {
80
82
  dto.id = slot.id;
81
83
  if (slot.unvalidated === true)
82
84
  dto.unvalidated = true;
85
+ if (slot.display === true)
86
+ dto.display = true;
83
87
  return dto;
84
88
  }
85
89
  function pageManifestToDTO(manifest) {
@@ -102,6 +106,8 @@ function pageManifestToDTO(manifest) {
102
106
  source.askedBy = manifest.source.askedBy;
103
107
  if (manifest.source.blockedSince !== undefined)
104
108
  source.blockedSince = manifest.source.blockedSince;
109
+ if (manifest.source.profileName !== undefined)
110
+ source.profileName = manifest.source.profileName;
105
111
  if (manifest.source.nodeId !== undefined)
106
112
  source.nodeId = manifest.source.nodeId;
107
113
  if (Object.keys(source).length > 0)
@@ -163,7 +169,7 @@ function isSlotValidationError(err) {
163
169
  // GET /v1/human/inbox — enumerate
164
170
  // ===========================================================================
165
171
  function handleList() {
166
- const productKinds = readConfig('user').page_components ?? [];
172
+ const productKinds = readConfig('user').page_components;
167
173
  const allItems = scanInbox(productKinds);
168
174
  const filtered = filterTerminalReviewTickets(allItems);
169
175
  const tickets = filtered.map((item) => {
@@ -210,7 +216,7 @@ function handleHistory(ctx) {
210
216
  if (!nodeId || typeof nodeId !== 'string') {
211
217
  throw new ApiError(400, 'invalid_request', 'history requires a node_id query param');
212
218
  }
213
- const productKinds = readConfig('user').page_components ?? [];
219
+ const productKinds = readConfig('user').page_components;
214
220
  const history = scanPageHistory(nodeId, productKinds);
215
221
  const canonicalRoot = realpathSync(ticketsRoot());
216
222
  const tickets = history.map((item) => {
@@ -230,6 +236,8 @@ function handleHistory(ctx) {
230
236
  };
231
237
  if (item.subtitle !== '')
232
238
  dto.subtitle = item.subtitle;
239
+ if (item.answerDigest !== undefined)
240
+ dto.answer_digest = item.answerDigest;
233
241
  return dto;
234
242
  });
235
243
  return { status: 200, body: { tickets } };
@@ -245,7 +253,7 @@ function handleGetPage(ctx) {
245
253
  if (resolved.kind === 'review') {
246
254
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page read operation');
247
255
  }
248
- const productKinds = readConfig('user').page_components ?? [];
256
+ const productKinds = readConfig('user').page_components;
249
257
  let ticket;
250
258
  try {
251
259
  ticket = readPageTicket(resolved.dir, productKinds);
@@ -287,7 +295,7 @@ async function handleRespond(ctx) {
287
295
  if (result?.kind === 'canceled') {
288
296
  throw new ApiError(409, 'ticket_canceled', 'canceled tickets cannot be answered');
289
297
  }
290
- const productKinds = readConfig('user').page_components ?? [];
298
+ const productKinds = readConfig('user').page_components;
291
299
  try {
292
300
  validatePageTicketResponses(resolved.dir, request.responses, productKinds);
293
301
  }
@@ -330,7 +338,7 @@ function handleProgress(ctx) {
330
338
  if (result !== null && result.kind !== 'canceled') {
331
339
  throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
332
340
  }
333
- const productKinds = readConfig('user').page_components ?? [];
341
+ const productKinds = readConfig('user').page_components;
334
342
  try {
335
343
  writePageProgress(resolved.dir, request.responses, productKinds);
336
344
  return { status: 200, body: {} };
@@ -390,7 +398,14 @@ function handleGetResponse(ctx) {
390
398
  if (result.kind !== 'page') {
391
399
  throw new ApiError(409, 'ticket_kind_mismatch', 'unexpected result kind');
392
400
  }
393
- return { status: 200, body: result };
401
+ const productKinds = readConfig('user').page_components;
402
+ try {
403
+ const manifest = parsePage(resolved.dir, productKinds);
404
+ return { status: 200, body: { ...result, answer: describePageAnswer(manifest, result.responses, { completedAt: result.completedAt, productKinds }) } };
405
+ }
406
+ catch {
407
+ throw new ApiError(500, 'internal', 'the ticket answer could not be read');
408
+ }
394
409
  }
395
410
  // ===========================================================================
396
411
  // GET /v1/human/inbox/:ticket_id/render — the page's HTML payload
@@ -403,7 +418,7 @@ function handleRenderPage(ctx) {
403
418
  if (resolved.kind === 'review') {
404
419
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page render operation');
405
420
  }
406
- const productKinds = readConfig('user').page_components ?? [];
421
+ const productKinds = readConfig('user').page_components;
407
422
  // Addressable for pending, resolved, and canceled tickets alike — the same
408
423
  // read rule as the detail route: a page stays readable after it resolves.
409
424
  let ticket;
@@ -163,6 +163,16 @@ function resolveLiveProfile(c) {
163
163
  return null;
164
164
  }
165
165
  }
166
+ function resolveLiveProfileName(c) {
167
+ if (c.profile == null)
168
+ return undefined;
169
+ try {
170
+ return loadProfileManifest(c.profile).manifest.name;
171
+ }
172
+ catch {
173
+ return undefined;
174
+ }
175
+ }
166
176
  // ---------------------------------------------------------------------------
167
177
  // Delivery sinks
168
178
  // ---------------------------------------------------------------------------
@@ -223,10 +233,14 @@ function deliverHumanSink(c, stdout) {
223
233
  // half-built `nfy-*` directory behind.
224
234
  let prepared;
225
235
  try {
236
+ const profileName = resolveLiveProfileName(c);
226
237
  prepared = preparePage({
227
238
  sourceFile: synthesized.path,
228
- dialect: 'md',
229
- source: { sessionName: `cron ${c.name} (${c.cron_id})` },
239
+ dialect: 'html',
240
+ source: {
241
+ sessionName: `cron ${c.name} (${c.cron_id})`,
242
+ ...(profileName === undefined ? {} : { profileName }),
243
+ },
230
244
  productKinds: readConfig('user').page_components,
231
245
  });
232
246
  }
@@ -8,7 +8,7 @@
8
8
  // The fleet registry's entry IS the node's liveness — no pid probe, no grace
9
9
  // clock. A broker's real `exit` event enqueues an exit-policy job on the
10
10
  // daemon's serial work lane (never inline on the Node event callback), where
11
- // classifyDeadNode — one pure 9-row policy table over durable row state —
11
+ // classifyDeadNode — one pure 8-row policy table over durable row state —
12
12
  // decides forget / dormant / respawn / boot-failure. The startup recovery
13
13
  // sweep classifies boot-time dead rows through the SAME table, so exit
14
14
  // recovery and startup recovery can never diverge. Every broker's argv is
@@ -10,12 +10,16 @@ import type { HostHandle } from '../core/runtime/host.js';
10
10
  * seeing a silent `dead`. */
11
11
  export declare function surfaceBootFailure(meta: NodeMeta): Promise<void>;
12
12
  /** Whether a dead relaunch should resume the saved session strictly, or retry
13
- * as a fresh cycle instead. A cycling attempt (cycle_pending) that dies
14
- * before session_start must retry AS a cycle — a strict reopen only restores
15
- * the transcript at an idle prompt, it cannot restart the dead engine turn.
16
- * A dirty interrupted BUSY turn also requests a cycle; a broker-certified
17
- * clean abort is handled explicitly by classifyDeadNode before this fallback. */
18
- export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null, interruptedTurn?: boolean): boolean;
13
+ * as a fresh cycle instead. A cycling attempt (cycle_pending) that dies before
14
+ * session_start must retry AS a cycle: the fresh branch it was opening is not
15
+ * confirmed, so resuming would silently continue the old leaf the refresh was
16
+ * meant to leave behind.
17
+ *
18
+ * An interrupted turn is NOT a reason to cycle. A strict reopen does restore
19
+ * the transcript at an idle prompt without restarting the dead turn — but the
20
+ * fix for that is the continuation prompt recovery delivers (see `#respawn`),
21
+ * not throwing the conversation away. */
22
+ export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null): boolean;
19
23
  export type DeadNodeAction = 'forget' | 'respawn-fresh' | 'dormant' | 'respawn-cycle' | 'respawn-resume' | 'boot-failure';
20
24
  /** The narrow durable state the policy table reads. Structural (a Pick, not
21
25
  * NodeMeta itself) so tests fabricate table rows directly. */
@@ -23,10 +27,18 @@ export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' |
23
27
  /** One pure function producing the action for a node known-dead. Used at
24
28
  * exactly two call sites — an exit-policy job and the startup recovery sweep
25
29
  * (design D-12) — so exit recovery and startup recovery can never diverge.
26
- * Evaluated in table order; `busy` is the on-disk busy marker. A clean-abort
27
- * certificate is written by the broker only after Pi persisted its aborted
28
- * entry; without it, a set busy marker means a dirty mid-turn crash. */
29
- export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean, cleanAbort?: boolean): DeadNodeAction;
30
+ * Evaluated in table order; `busy` is the on-disk busy marker.
31
+ *
32
+ * `busy` no longer selects the ACTION. Whether the engine died mid-turn or
33
+ * parked at an idle prompt, the saved transcript is equally resumable — pi
34
+ * persists whole entries (`appendFileSync` per line) so a killed generation
35
+ * simply leaves the file ending at its last complete entry, and the loader
36
+ * skips a malformed line rather than rejecting the session. A mid-turn death
37
+ * needs the turn re-driven, which is a continuation prompt on top of the
38
+ * resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
39
+ * only to route a node that died before session_start ever recorded a session
40
+ * to a fresh relaunch instead of terminalizing it as a boot failure. */
41
+ export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean): DeadNodeAction;
30
42
  /** An exit with uptime at or above this resets the consecutive-failure
31
43
  * counter before classification; a shorter-lived exit increments it. */
32
44
  export declare const HEALTHY_UPTIME_MS = 60000;