@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
@@ -1,65 +1,17 @@
1
1
  import { readConfig } from '../../core/config.js';
2
- function markdownAuthoring(productKinds) {
3
- const productSlots = productKinds.length === 0
4
- ? ''
5
- : `\nProduct slots registered in this home: ${productKinds.map((kind) => `\`${kind}\``).join(', ')}. Use \`slot:<kind>\`; each is response-bearing.`;
6
- return [
7
- 'Markdown page (`.md`; one step only)',
8
- 'Front matter starts at byte 0 and recognizes exactly `title` (required), `subtitle` (optional), and `placement` (`inline|panel`, optional, default `panel`); every other key is invalid. Outside front matter, write directive-flavored Markdown. A top-level `slot:<kind>` JSON fence contains one object: reserved top-level `id` identifies a response-bearing slot; every other key is that kind\'s config.',
9
- 'Built-ins: `options` asks for one or more choices; `text` asks for text; `table` asks for row and/or column selections when enabled, otherwise displays data; `cards` asks for card selections; `chart` displays a chart and asks nothing.' + productSlots,
10
- 'Format specimen:',
11
- '````markdown',
12
- '---',
13
- 'title: Rollout scope',
14
- 'subtitle: Choose tonight\'s rollout.',
15
- 'placement: inline',
16
- '---',
17
- '',
18
- 'Which rollout should ship?',
19
- '',
20
- '```slot:options',
21
- '{"id":"rollout","mode":"single","options":[{"id":"canary","label":"Canary first"}]}',
22
- '```',
23
- '````',
24
- ].join('\n');
25
- }
26
- function htmlAuthoring(productKinds) {
27
- const productSlot = productKinds.length === 0
28
- ? ''
29
- : ' Product kinds use `<crtr-slot kind="<kind>" id="…">` with the same JSON-script child.';
30
- return [
31
- 'HTML page (`.html` or `.htm`)',
32
- 'Put required title and optional subtitle in `<head>` metas named `crtr-page-title` and `crtr-page-subtitle`. Placement is declared only by `<meta name="crtr-page-placement" content="inline|panel">` in `<head>`; it defaults to `panel`.',
33
- 'Built-ins are `<crtr-options>`, `<crtr-text>`, `<crtr-table>`, `<crtr-cards>`, and `<crtr-chart>`; `id` is an attribute and config is the first direct `<script type="application/json">` child.' + productSlot,
34
- 'An interactive page (any response-bearing slot), or any multi-step page, requires one `<crtr-pages>` wrapper containing one or more `<crtr-page>` steps, with every response-bearing element inside a step. A pure display page is correctly bare HTML with no `crtr-*` markup at all.',
35
- 'Format specimen:',
36
- '```html',
37
- '<!doctype html>',
38
- '<html><head>',
39
- ' <meta name="crtr-page-title" content="Rollout scope">',
40
- ' <meta name="crtr-page-placement" content="inline">',
41
- '</head><body>',
42
- ' <crtr-pages><crtr-page>',
43
- ' <crtr-options id="rollout"><script type="application/json">{"mode":"single","options":[{"id":"canary","label":"Canary first"}]}</script></crtr-options>',
44
- ' </crtr-page></crtr-pages>',
45
- '</body></html>',
46
- '```',
47
- ].join('\n');
48
- }
49
- /** Invocation-local page capability and authoring contract for `human -h`.
50
- * The CLI process rebuilds command help each time, so config changes appear on
51
- * the next invocation without a parallel documentation surface. */
52
- export function pageHelpText() {
53
- const config = readConfig('user');
54
- const hardRule = '**Hard rule:** write the page to a local file and submit it with `--page PATH`; NEVER paste page markup into a message.';
55
- const shorthand = 'Zero ceremony: `ask` stdin plus `--title` and `--subtitle` synthesizes a one-text-slot page; `notify` title plus `--subtitle` and optional `--body` synthesizes a display page.';
56
- const dialects = config.html_pages
57
- ? [markdownAuthoring(config.page_components), htmlAuthoring(config.page_components)]
58
- : [markdownAuthoring(config.page_components)];
59
- const pageHint = config.html_pages
60
- ? 'Authored page file. This home accepts .md, .html, and .htm; the extension selects the dialect.'
61
- : 'Authored Markdown page file. This home has html_pages off, so only .md is accepted.';
62
- return { authoring: [hardRule, shorthand, ...dialects].join('\n\n'), pageHint };
2
+ import { BUILTIN_COMPONENT_SELECTION_LINES } from '../../core/human/component-docs.js';
3
+ /** The one HTML authoring model for `human -h`. */
4
+ export function pageHelpText(htmlSurface = false) {
5
+ return {
6
+ authoring: htmlSurface
7
+ ? 'Use inline HTML pages as the default way to put structured or interactive content in front of the human; the environment renders web content, so a page is not a special occasion. Write one complete HTML document to `$CRTR_CONTEXT_DIR/pages/<name>.html`. Its `<head>` has required `<meta name="crtr-page-title" content="…">`, optional `<meta name="crtr-page-subtitle" content="…">`, and optional `<meta name="crtr-page-placement" content="inline|panel">` (default `panel`); interactive or multi-step pages need one `<crtr-pages>` wrapper with `<crtr-page>` steps, and each component config is its first `<script type="application/json">` child. Display-only pages are first-class: a page with no questions is a legitimate, encouraged way to show structured UI with `notify --page`.'
8
+ : 'Write one complete HTML document to `$CRTR_CONTEXT_DIR/pages/<name>.html`. Its `<head>` has required `<meta name="crtr-page-title" content="…">`, optional `<meta name="crtr-page-subtitle" content="…">`, and optional `<meta name="crtr-page-placement" content="inline|panel">` (default `panel`); interactive or multi-step pages need one `<crtr-pages>` wrapper with `<crtr-page>` steps, and each component config is its first `<script type="application/json">` child. Display-only pages are first-class: a page with no questions is a legitimate, encouraged way to show structured UI with `notify --page`.',
9
+ componentSelection: BUILTIN_COMPONENT_SELECTION_LINES,
10
+ pageHint: 'Write the page to `$CRTR_CONTEXT_DIR/pages/<name>.html`; pass that path.',
11
+ rootUseWhen: htmlSurface
12
+ ? 'when a person must decide, review a document with you, or receive a live display; use inline HTML pages by default for structured or interactive content because the environment renders web content.'
13
+ : 'when a person must decide, review a document with you, or receive a live display.',
14
+ };
63
15
  }
64
16
  export function resolveMaxPanes() {
65
17
  return readConfig('user').max_panes_per_window;
@@ -4,20 +4,22 @@
4
4
  // asks crtrd to create a ticket, daemon-owned review, and companion. notify
5
5
  // enqueues a durable page with no node; show is passive display.
6
6
  import { defineBranch } from '../core/command.js';
7
+ import { readConfig } from '../core/config.js';
7
8
  import { humanAsk, humanNotify, humanShow } from './human/prompts.js';
8
9
  import { humanList, humanResolve, humanCancel } from './human/queue.js';
9
10
  import { humanReviewBranch } from './human/review.js';
10
11
  import { humanInbox } from './human/inbox.js';
11
12
  import { humanDoc } from './human/doc.js';
13
+ import { humanHtml } from './human/html.js';
12
14
  import { pageHelpText } from './human/shared.js';
13
15
  export function registerHuman() {
14
- const pageHelp = pageHelpText();
16
+ const pageHelp = pageHelpText(readConfig('user').html_surface);
15
17
  return defineBranch({
16
18
  name: 'human',
17
19
  rootEntry: {
18
20
  concept: 'human-in-the-loop decisions, document review, and live display',
19
21
  desc: 'ask, review new/comment, notify, show, cancel, list',
20
- useWhen: 'when a person must decide, review a document with you, or receive a live display.'
22
+ useWhen: pageHelp.rootUseWhen,
21
23
  },
22
24
  help: {
23
25
  name: 'human',
@@ -25,9 +27,12 @@ export function registerHuman() {
25
27
  model: [
26
28
  "Tickets queue in the human inbox; nothing opens on screen. `ask` returns immediately and its answer is pushed to the asking node, while `notify` expects no reply. `review` is for live document review with anchored comments; `show` is a passive tmux live-watch rather than a ticket. Validate authored pages with `crtr human doc check`.",
27
29
  pageHelp.authoring,
30
+ pageHelp.componentSelection.join('\n'),
31
+ 'Component contracts: `crtr human html`.',
28
32
  ].join('\n\n'),
29
33
  },
30
34
  children: [
35
+ humanHtml,
31
36
  humanAsk,
32
37
  humanReviewBranch,
33
38
  humanNotify,
@@ -131,8 +131,11 @@ test('an unplaced active node is left alone before it boots', async () => {
131
131
  // node with no fleet entry must NOT be spuriously revived by the tick. Pass-2
132
132
  // is the ONLY wake path for a released node, and it fires solely on an unseen
133
133
  // inbox entry — a null recorded pid plus a saved session must never read as a
134
- // "stranded relaunch" (that inference branch is deleted).
135
- test('idle-release node with a boot-cleared (null) pid and no unseen inbox stays dormant', async () => {
134
+ // "stranded relaunch" (that inference branch is deleted). With nothing left to
135
+ // wake it and no live subscriber awaiting its report, the same pass settles it
136
+ // as a terminal node torn down before finishing rather than leaving a
137
+ // permanently protected idle row.
138
+ test('idle-release node with a boot-cleared (null) pid and no unseen inbox is settled, never revived', async () => {
136
139
  createNode(node('D', {
137
140
  status: 'idle',
138
141
  intent: 'idle-release',
@@ -143,8 +146,8 @@ test('idle-release node with a boot-cleared (null) pid and no unseen inbox stays
143
146
  await superviseTick(t0); // first observation
144
147
  await superviseTick(t0 + 60_000); // well past the boot grace
145
148
  const after = getNode('D');
146
- assert.equal(after.status, 'idle', 'dormant idle-release node is left idle, not revived');
147
- assert.equal(after.intent, 'idle-release', 'intent unchanged — no revive fired');
149
+ assert.equal(after.status, 'canceled', 'a dormant terminal row nobody awaits becomes prunable canceled history');
150
+ assert.equal(after.intent, null, 'no revive fired — the row is settled, not relaunched');
148
151
  assert.equal(after.pi_pid, null, 'no fresh pid — reviveNode was never invoked');
149
152
  });
150
153
  // Daemon start guard: spawnDaemon must not report success before the daemon is
@@ -4,11 +4,7 @@
4
4
  // pure function mapping a known-dead node's durable row state to its recovery
5
5
  // action, used at exactly two call sites — the exit-policy job and the startup
6
6
  // recovery sweep — so exit recovery and startup recovery can never diverge.
7
- // This file pins every table row directly, replacing the scattered pins the
8
- // five deleted liveness-inference branches used to carry (grace-clock,
9
- // stranded-relaunch, daemon-liveness), plus the `retryResumeMode` policy pins
10
- // ported verbatim from the deleted grace-clock suite (the function is pure and
11
- // unchanged — only its caller moved into the table).
7
+ // This file pins every table row directly.
12
8
  import { test } from 'node:test';
13
9
  import assert from 'node:assert/strict';
14
10
  import { classifyDeadNode, retryResumeMode } from '../../daemon/fleet.js';
@@ -24,7 +20,7 @@ function row(over = {}) {
24
20
  };
25
21
  }
26
22
  // ===========================================================================
27
- // The nine table rows, in evaluation order (design flow B).
23
+ // The eight table rows, in evaluation order (design flow B).
28
24
  // ===========================================================================
29
25
  test('row 1: a deleted row (orphan of a pruned/closed node) is forgotten', () => {
30
26
  assert.equal(classifyDeadNode(null, false), 'forget');
@@ -44,42 +40,30 @@ test('row 4: intent=idle-release (chosen dormancy) takes no action — pass-2 in
44
40
  assert.equal(classifyDeadNode(row({ intent: 'idle-release' }), false), 'dormant');
45
41
  assert.equal(classifyDeadNode(row({ intent: 'idle-release', pi_session_id: 'sess' }), false), 'dormant', 'a saved session does not turn chosen dormancy into a crash');
46
42
  });
47
- test('row 5: a broker-certified clean abort resumes its persisted session', () => {
48
- assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), true, true), 'respawn-resume');
49
- assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: true }), true, true), 'respawn-resume', 'the clean-abort certificate proves this interrupted turn is resumable, even if a prior cycle was pending');
50
- assert.equal(classifyDeadNode(row(), true, true), 'respawn-cycle', 'a certificate without a saved session cannot turn a dirty busy death into a resume');
51
- });
52
- test('row 6: a set busy marker without clean-abort evidence is a dirty mid-turn crash — ALWAYS a recovery cycle', () => {
53
- assert.equal(classifyDeadNode(row(), true), 'respawn-cycle');
54
- assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), true), 'respawn-cycle');
55
- assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: true }), true), 'respawn-cycle');
56
- });
57
- test('row 7: a saved session with no crash markers resumes strictly; a pending cycle retries AS a cycle', () => {
43
+ test('row 5: every saved session resumes strictly unless its own pending cycle must be retried', () => {
58
44
  assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), false), 'respawn-resume');
59
- assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: true }), false), 'respawn-cycle');
60
- assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: false }), false), 'respawn-resume');
45
+ assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), true), 'respawn-resume', 'a dirty interrupted turn resumes in place; the respawn continuation re-drives it');
46
+ assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: false }), true), 'respawn-resume');
47
+ assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: true }), true), 'respawn-cycle', 'an interrupted unconfirmed cycling attempt still retries AS that cycle');
48
+ });
49
+ test('row 6: a sessionless node that died mid-turn relaunches fresh instead of becoming a boot failure', () => {
50
+ assert.equal(classifyDeadNode(row(), true), 'respawn-fresh');
61
51
  });
62
- test('row 8: an ordinary fork dead before binding its child session retries the immutable fork birth', () => {
52
+ test('row 7: an ordinary fork dead before binding its child session retries the immutable fork birth', () => {
63
53
  assert.equal(classifyDeadNode(row({ fork_from: '/reviews/branch-point.jsonl' }), false), 'respawn-fresh');
64
54
  });
65
- test('row 9: sessionless with no fork claim — a fresh launch died before session_start — is a boot failure', () => {
55
+ test('row 8: sessionless with no fork claim — a fresh launch died before session_start — is a boot failure', () => {
66
56
  assert.equal(classifyDeadNode(row(), false), 'boot-failure');
67
57
  });
68
58
  // ===========================================================================
69
- // retryResumeMode (pure) — ported verbatim from the deleted grace-clock suite.
70
- // A crash-retry retries AS A CYCLE (resume:false) when the dead attempt's own
71
- // launch was itself a cycling revive that never reached session_start
72
- // (cycle_pending), OR when a surviving busy marker proves the broker died
73
- // mid-turn. Idle crashes resume STRICTLY (resume:true). See canvas.test.ts's
74
- // updateNode/getNode round-trip for the companion guarantee that cycle_pending
75
- // survives the persistence the policy job reads it back from.
59
+ // retryResumeMode (pure): only an unconfirmed cycling launch must cycle again.
60
+ // An interrupted ordinary turn resumes strictly; `#respawn` adds the runtime
61
+ // continuation that re-drives the stopped work in the same conversation.
76
62
  // ===========================================================================
77
- test('retryResumeMode: interrupted turns and pending cycles recover as cycles; idle crashes resume strictly', () => {
78
- assert.equal(retryResumeMode({ cycle_pending: true }, false), false, 'a pending cycling attempt retries AS a cycle');
79
- assert.equal(retryResumeMode({ cycle_pending: false }, true), false, 'a surviving busy marker starts a recovery cycle');
80
- assert.equal(retryResumeMode({ cycle_pending: true }, true), false, 'both recovery signals still produce one cycle');
81
- assert.equal(retryResumeMode({ cycle_pending: false }, false), true, 'an idle ordinary crash resumes strictly');
82
- assert.equal(retryResumeMode({ cycle_pending: null }, false), true, 'cycle_pending=null idle crash resumes strictly');
83
- assert.equal(retryResumeMode({}, false), true, 'cycle_pending undefined idle crash resumes strictly');
84
- assert.equal(retryResumeMode(null, false), true, 'no meta and no busy marker resumes strictly');
63
+ test('retryResumeMode: pending cycles recover as cycles; every confirmed saved session resumes strictly', () => {
64
+ assert.equal(retryResumeMode({ cycle_pending: true }), false, 'a pending cycling attempt retries AS a cycle');
65
+ assert.equal(retryResumeMode({ cycle_pending: false }), true, 'an ordinary saved session resumes strictly');
66
+ assert.equal(retryResumeMode({ cycle_pending: null }), true, 'cycle_pending=null resumes strictly');
67
+ assert.equal(retryResumeMode({}), true, 'cycle_pending undefined resumes strictly');
68
+ assert.equal(retryResumeMode(null), true, 'no pending cycle resumes strictly');
85
69
  });
@@ -6,7 +6,6 @@ import assert from 'node:assert/strict';
6
6
  import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
- import { Readable } from 'node:stream';
10
9
  import { createApiServer } from '../../daemon/api/server.js';
11
10
  import { cancelHumanTicket, deliverTerminalResult, resolvePageTicket } from '../../daemon/human/finish.js';
12
11
  import { humanAsk, humanNotify } from '../../commands/human/prompts.js';
@@ -28,18 +27,6 @@ async function waitForSocket(path) {
28
27
  }
29
28
  throw new Error(`api socket never bound at ${path}`);
30
29
  }
31
- async function withPipedStdin(body, fn) {
32
- const original = process.stdin;
33
- const stdin = Readable.from([Buffer.from(body)]);
34
- Object.defineProperty(stdin, 'isTTY', { value: false, configurable: true });
35
- Object.defineProperty(process, 'stdin', { value: stdin, configurable: true });
36
- try {
37
- return await fn();
38
- }
39
- finally {
40
- Object.defineProperty(process, 'stdin', { value: original, configurable: true });
41
- }
42
- }
43
30
  async function withMutedStdout(fn) {
44
31
  const originalWrite = process.stdout.write;
45
32
  process.stdout.write = (() => true);
@@ -58,20 +45,10 @@ function bridgeUnder() {
58
45
  function submitBridgePage(bridge) {
59
46
  const dir = ticketDir(bridge);
60
47
  const sourceDir = mkdtempSync(join(tmpdir(), 'crtr-deliver-page-'));
61
- const sourceFile = join(sourceDir, 'page.md');
62
- writeFileSync(sourceFile, [
63
- '---',
64
- 'title: Ship it?',
65
- 'subtitle: Decide whether to ship now so the release can proceed.',
66
- '---',
67
- '',
68
- '```slot:text',
69
- '{ "id": "question", "initialText": "", "editable": true }',
70
- '```',
71
- '',
72
- ].join('\n'));
48
+ const sourceFile = join(sourceDir, 'page.html');
49
+ writeFileSync(sourceFile, '<!doctype html><html><head><meta name="crtr-page-title" content="Ship it?"><meta name="crtr-page-subtitle" content="Decide whether to ship now so the release can proceed."></head><body><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":"","editable":true}</script></crtr-text></crtr-page></crtr-pages></body></html>');
73
50
  try {
74
- submitPage({ dir, sourceFile, dialect: 'md' });
51
+ submitPage({ dir, sourceFile, dialect: 'html' });
75
52
  }
76
53
  finally {
77
54
  rmSync(sourceDir, { recursive: true, force: true });
@@ -110,11 +87,14 @@ after(() => {
110
87
  delete process.env['CRTR_NODE_ID'];
111
88
  });
112
89
  describe('enqueue is producer-only', () => {
113
- test('ask requires and preserves an explicit title instead of deriving one from the body', async () => {
90
+ test('ask preserves title and subtitle from the authored page', async () => {
114
91
  const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
115
92
  process.env['CRTR_NODE_ID'] = parent;
116
- await assert.rejects(() => humanAsk.run({ question: 'This first line must never become the title.' }), /require a non-empty --title/);
117
- const result = (await humanAsk.run({ title: 'Release decision', subtitle: 'Decide whether to release now so the launch can proceed.', question: 'This first line must never become the title.' }));
93
+ const page = join(home, 'release-decision.html');
94
+ writeFileSync(page, '<!doctype html><html><head><meta name="crtr-page-title" content="Release decision"><meta name="crtr-page-subtitle" content="Decide whether to release now so the launch can proceed."></head><body><p>This first line stays page content.</p><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":"","editable":true}</script></crtr-text></crtr-page></crtr-pages></body></html>');
95
+ await assert.rejects(() => humanAsk.run({}), /a page source is required/);
96
+ await assert.rejects(() => humanAsk.run({ page, document: '<!doctype html><html></html>' }), /received both --page and a piped page document/);
97
+ const result = (await humanAsk.run({ page, document: '' }));
118
98
  assert.equal(typeof result['job_id'], 'string');
119
99
  assert.equal(typeof result['dir'], 'string');
120
100
  for (const forbidden of ['pane_id', 'render_pane_id', 'pid', 'worker_pid'])
@@ -122,9 +102,18 @@ describe('enqueue is producer-only', () => {
122
102
  const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
123
103
  assert.equal(manifest.title, 'Release decision');
124
104
  assert.equal(manifest.subtitle, 'Decide whether to release now so the launch can proceed.');
125
- const document = readFileSync(join(result['dir'], 'page.md'), 'utf8');
126
- assert.ok(document.includes('This first line must never become the title.'));
127
- assert.equal(manifest.slots.filter((slot) => slot.kind === 'text').length, 1, 'the synthesized question page carries one response slot');
105
+ const document = readFileSync(join(result['dir'], 'page.html'), 'utf8');
106
+ assert.ok(document.includes('This first line stays page content.'));
107
+ assert.equal(manifest.slots.filter((slot) => slot.kind === 'text').length, 1);
108
+ });
109
+ test('ask materializes a piped page before validation and publication', async () => {
110
+ const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
111
+ process.env['CRTR_NODE_ID'] = parent;
112
+ const document = '<!doctype html><html><head><meta name="crtr-page-title" content="Piped question"><meta name="crtr-page-subtitle" content="Answer the question to unblock this task."></head><body><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":""}</script></crtr-text></crtr-page></crtr-pages></body></html>';
113
+ const result = (await humanAsk.run({ document }));
114
+ const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
115
+ assert.equal(manifest.title, 'Piped question');
116
+ assert.equal(readFileSync(join(result['dir'], 'page.html'), 'utf8'), document);
128
117
  });
129
118
  });
130
119
  describe('terminal ticket delivery', () => {
@@ -138,7 +127,7 @@ describe('terminal ticket delivery', () => {
138
127
  const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
139
128
  assert.equal(finals.length, 1, 'exactly one pushFinal reached the parent');
140
129
  const body = reportBody(finals[0].ref);
141
- assert.ok(body.includes('"text":"yes, ship"'), 'the slot answer is inlined in the report');
130
+ assert.ok(body.includes('“yes, ship”'), 'the readable slot answer is inlined in the report');
142
131
  assert.ok(body.includes(join(dir, 'response.json')));
143
132
  });
144
133
  test('a replay against a terminal bridge is a no-op', async () => {
@@ -162,14 +151,16 @@ describe('terminal ticket delivery', () => {
162
151
  assert.equal(getNode(bridge)?.status, 'done');
163
152
  assert.equal(readInboxSince(actor).length, 0, 'the canceling actor is not messaged');
164
153
  const notes = readInboxSince(other);
165
- assert.equal(notes.length, 1, 'the other waiter gets one deferred note');
166
- assert.equal(notes[0].tier, 'deferred');
154
+ assert.equal(notes.length, 1, 'the other waiter gets one waking note');
155
+ assert.equal(notes[0].tier, 'normal');
167
156
  assert.match(notes[0].label, /canceled/);
168
157
  });
169
158
  });
170
- test('notify enqueues a durable page and creates no canvas node', async () => {
159
+ test('notify enqueues an authored page and creates no canvas node', async () => {
160
+ const page = join(home, 'build-done.html');
161
+ writeFileSync(page, '<!doctype html><html><head><meta name="crtr-page-title" content="Build done"><meta name="crtr-page-subtitle" content="The build passed, so the change is ready to review."></head><body><p>Build output is ready.</p></body></html>');
171
162
  const before = listNodes().length;
172
- const result = (await humanNotify.run({ title: 'Build done', subtitle: 'The build passed, so the change is ready to review.' }));
163
+ const result = (await humanNotify.run({ page }));
173
164
  assert.equal(result['queued'], true);
174
165
  assert.equal(typeof result['dir'], 'string');
175
166
  assert.ok(existsSync(join(result['dir'], 'page.json')));
@@ -2,9 +2,10 @@
2
2
  //
3
3
  // Daemon-epoch broker ownership, real-process crash and teardown. The broker's
4
4
  // exit event is the sole death authority: a SIGTERM of a streaming broker
5
- // aborts cleanly and resumes the saved session with a continuation (policy row
6
- // 5); a SIGKILL mid-turn leaves no certificate and starts a fresh cycle (row
7
- // 6). Each replacement starts only after the
5
+ // aborts cleanly and resumes the saved session with a continuation, and a
6
+ // SIGKILL mid-turn — no certificate, only the durable busy marker — resumes
7
+ // that same saved session in place and re-drives the stopped turn with the
8
+ // same continuation (both policy row 5). Each replacement starts only after the
8
9
  // prior process has exited, preserving one writer per session. A clean `node
9
10
  // lifecycle close` then tears the replacement down (shutdown frame → exit,
10
11
  // socket unlinked, status canceled, daemon leaves it).
@@ -77,7 +78,7 @@ test('SIGTERM of a busy broker aborts cleanly and resumes with a continuation',
77
78
  assert.notEqual(revivedPid, oldPid, 'the revived pid is distinct from the crashed one');
78
79
  assert.equal(isPidAlive(oldPid), false, 'one-writer: the crashed pid never resurrected');
79
80
  });
80
- test('SIGKILL of a busy broker stays a dirty recovery cycle; close tears down the replacement', { timeout: 30_000 }, async () => {
81
+ test('SIGKILL of a busy broker resumes the interrupted turn in place; close tears down the replacement', { timeout: 30_000 }, async () => {
81
82
  const id = await h.spawnHeadlessChild(root, 'headless worker — dirty crash+teardown');
82
83
  const boot1 = await h.awaitBoot(id);
83
84
  const boots = h.bootCount(id);
@@ -85,9 +86,10 @@ test('SIGKILL of a busy broker stays a dirty recovery cycle; close tears down th
85
86
  assert.equal(busyPid, boot1.pid, 'canvas pid identifies the broker that owns the dirty-crash session');
86
87
  assert.equal(isPidAlive(busyPid), true, 'busy broker is alive before SIGKILL');
87
88
  // This broker has only the durable busy marker — SIGKILL cannot run the abort
88
- // handler or write clean-abort evidence. Strict resume would only reopen the
89
- // transcript at an idle prompt; policy must select resume:false and inject the
90
- // normal fresh-cycle kickoff after this journey's first 5s backoff.
89
+ // handler or write clean-abort evidence. The saved transcript is still
90
+ // resumable (pi persists whole entries), so policy resumes it strictly and
91
+ // the respawn continuation re-drives the stopped turn, after this journey's
92
+ // first 5s backoff. A dirty death is not a reason to throw the branch away.
91
93
  markBusy(id);
92
94
  process.kill(busyPid, 'SIGKILL');
93
95
  await h.waitFor(() => !isPidAlive(busyPid), { label: 'mid-turn crashed broker pid is dead' });
@@ -96,11 +98,11 @@ test('SIGKILL of a busy broker stays a dirty recovery cycle; close tears down th
96
98
  // path, including a cycle; production distinguishes cycling with the argv
97
99
  // token and non-null kickoff. Assert those load-bearing inputs directly.
98
100
  const recoveryArgv = boot2.rawArgv;
99
- assert.ok(recoveryArgv.includes('--crtr-new-cycle'), 'a surviving busy marker selects --crtr-new-cycle');
100
- assert.match(boot2.prompt ?? '', /You have been revived fresh after a context refresh/, 'the recovery cycle receives a real roadmap\/feed\/context kickoff');
101
+ assert.ok(!recoveryArgv.includes('--crtr-new-cycle'), 'a surviving busy marker resumes the saved session — it never opens a fresh cycle');
102
+ assert.equal(boot2.prompt, RUNTIME_RESTART_CONTINUATION, 'the strict resume injects the runtime-restart continuation that re-drives the interrupted turn');
101
103
  const recoveryPid = h.node(id).pi_pid;
102
- assert.ok(recoveryPid != null && isPidAlive(recoveryPid), 'the recovery-cycle broker pid is alive');
103
- assert.notEqual(recoveryPid, busyPid, 'the recovery cycle uses a distinct broker pid');
104
+ assert.ok(recoveryPid != null && isPidAlive(recoveryPid), 'the recovered broker pid is alive');
105
+ assert.notEqual(recoveryPid, busyPid, 'the recovery uses a distinct broker pid');
104
106
  assert.equal(isPidAlive(busyPid), false, 'one-writer: the interrupted pid never resurrected');
105
107
  // --- Item 5: clean teardown of the (revived, live) broker. node lifecycle close marks
106
108
  // canceled BEFORE teardown (crash-safe order), then hostFor(broker).teardown
@@ -7,16 +7,20 @@
7
7
  // before the broker exits, and the daemon may only revive for a row strictly
8
8
  // after the committed cursor.
9
9
  // (2) The observed 57-broker / ~5.4GB leak — DAEMON SUPERVISION, not an agent
10
- // event, owns the unattended clock. A resident with no live obligation
11
- // completes into prunable history; one awaiting a child remains wakeable.
12
- // Both release their broker and fleet slot.
10
+ // event, owns the unattended clock. A node with no live obligation is
11
+ // terminalized into prunable history (a resident completed its
12
+ // conversation; a terminal node parked without a final was torn down
13
+ // before finishing); one awaiting a child remains wakeable; an unclaimed
14
+ // warm-pool spare is exempt, because the booted broker it holds is its
15
+ // entire purpose. All of them release their broker and fleet slot except
16
+ // the spare, which keeps both.
13
17
  //
14
18
  // Both drive a real broker process through the real watcher/supervision paths, so
15
19
  // they belong to the compiled seam tier; neither needs tmux.
16
20
  import { after, before, test } from 'node:test';
17
21
  import assert from 'node:assert/strict';
18
22
  import { appendInbox, readCursor, readInboxSince } from '../../feed/inbox.js';
19
- import { subscribe, updateNode } from '../../canvas/index.js';
23
+ import { registerWarmSpare, subscribe, updateNode } from '../../canvas/index.js';
20
24
  import { isPidAlive } from '../../canvas/pid.js';
21
25
  import { reviveNode } from '../../runtime/revive.js';
22
26
  import { createHarness } from '../helpers/harness.js';
@@ -172,6 +176,61 @@ test('an unattended resident with no live obligation completes on the daemon clo
172
176
  process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
173
177
  }
174
178
  });
179
+ test('an already idle terminal node with no live obligation reconciles into canceled', async () => {
180
+ const nodeId = h.fabricateBrokerNode({
181
+ id: 'idle-terminal-without-obligation',
182
+ parent: root,
183
+ status: 'idle',
184
+ intent: 'idle-release',
185
+ pi_session_id: 'parked-terminal-session',
186
+ });
187
+ await h.tick();
188
+ assert.equal(h.node(nodeId)?.status, 'canceled', 'a terminal row that never pushed a final settles as torn down, not completed');
189
+ });
190
+ test('the unattended park clock cancels an obligation-free terminal node and spares the warm pool', { timeout: 40_000 }, async () => {
191
+ const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
192
+ process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
193
+ try {
194
+ const boot = async (id) => {
195
+ updateNode(id, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
196
+ reviveNode(id, { resume: true });
197
+ return (await h.awaitBoot(id)).pid;
198
+ };
199
+ const settled = h.fabricateBrokerNode({ id: 'terminal-no-work', parent: root, status: 'idle', intent: 'idle-release' });
200
+ const awaiting = h.fabricateBrokerNode({ id: 'terminal-awaiting-child', parent: root, status: 'idle', intent: 'idle-release' });
201
+ const child = h.fabricateBrokerNode({ parent: awaiting, status: 'idle', intent: 'idle-release' });
202
+ subscribe(awaiting, child, true);
203
+ // Production spares mint as unparented residents, so this is the row shape
204
+ // the park clock actually meets in the fleet.
205
+ const spare = h.fabricateBrokerNode({ id: 'unclaimed-warm-spare', lifecycle: 'resident', status: 'idle', intent: 'idle-release' });
206
+ registerWarmSpare(spare, 'test-recipe-key');
207
+ const settledPid = await boot(settled);
208
+ const awaitingPid = await boot(awaiting);
209
+ const sparePid = await boot(spare);
210
+ const seenAt = 30_000;
211
+ await h.tick(seenAt);
212
+ await h.tick(seenAt + 1500);
213
+ await h.waitFor(() => h.node(settled)?.status === 'canceled' && !isPidAlive(settledPid), {
214
+ timeoutMs: 10_000,
215
+ intervalMs: 20,
216
+ label: 'unattended terminal cancellation and broker exit',
217
+ });
218
+ await h.awaitFleetExit(settled);
219
+ assert.equal(h.node(settled)?.status, 'canceled', 'a terminal node torn down without a final is prunable canceled history');
220
+ await h.waitFor(() => !isPidAlive(awaitingPid), { timeoutMs: 10_000, intervalMs: 20, label: 'awaiting terminal release' });
221
+ await h.awaitFleetExit(awaiting);
222
+ assert.equal(h.node(awaiting)?.status, 'idle', 'a live child keeps a terminal node eligible for an automatic wake');
223
+ assert.equal(h.fleet.has(spare), true, 'the park clock never reaps an unclaimed spare out from under the pool');
224
+ assert.equal(isPidAlive(sparePid), true, 'the spare keeps the booted broker that is its whole value');
225
+ assert.ok(['active', 'idle'].includes(h.node(spare)?.status ?? ''), 'the spare row stays claimable, left for reapStaleSpares to own');
226
+ }
227
+ finally {
228
+ if (priorParkMs === undefined)
229
+ delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
230
+ else
231
+ process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
232
+ }
233
+ });
175
234
  test('an unattended resident awaiting a live child remains wakeable', { timeout: 30_000 }, async () => {
176
235
  const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
177
236
  process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
@@ -66,20 +66,10 @@ async function expectReviewSubmitGuidance(nodeId, body) {
66
66
  function submitBridgePage(bridge) {
67
67
  const dir = ticketDir(bridge);
68
68
  const sourceDir = mkdtempSync(join(tmpdir(), 'crtr-e2e-page-'));
69
- const sourceFile = join(sourceDir, 'page.md');
70
- writeFileSync(sourceFile, [
71
- '---',
72
- 'title: Ship it?',
73
- 'subtitle: Decide whether to ship now so the release can proceed.',
74
- '---',
75
- '',
76
- '```slot:text',
77
- '{ "id": "question", "initialText": "", "editable": true }',
78
- '```',
79
- '',
80
- ].join('\n'));
69
+ const sourceFile = join(sourceDir, 'page.html');
70
+ writeFileSync(sourceFile, '<!doctype html><html><head><meta name="crtr-page-title" content="Ship it?"><meta name="crtr-page-subtitle" content="Decide whether to ship now so the release can proceed."></head><body><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":"","editable":true}</script></crtr-text></crtr-page></crtr-pages></body></html>');
81
71
  try {
82
- submitPage({ dir, sourceFile, dialect: 'md' });
72
+ submitPage({ dir, sourceFile, dialect: 'html' });
83
73
  }
84
74
  finally {
85
75
  rmSync(sourceDir, { recursive: true, force: true });
@@ -27,11 +27,11 @@ function seedPage(bridgeId) {
27
27
  mkdirSync(dir, { recursive: true });
28
28
  atomicWriteJson(pageManifestPath(dir), {
29
29
  schema: 'crtr.page/v1',
30
- dialect: 'md',
30
+ dialect: 'html',
31
31
  title: bridgeId,
32
32
  subtitle: 'Acknowledge this ticket so the request can proceed.',
33
33
  placement: 'inline',
34
- document: 'page.md',
34
+ document: 'page.html',
35
35
  steps: 1,
36
36
  slots: [],
37
37
  });
@@ -115,11 +115,11 @@ function seedPoisonedLocalState() {
115
115
  mkdirSync(askDir, { recursive: true });
116
116
  atomicWriteJson(pageManifestPath(askDir), {
117
117
  schema: 'crtr.page/v1',
118
- dialect: 'md',
118
+ dialect: 'html',
119
119
  title: 'poison',
120
120
  subtitle: 'This local ticket must not affect the remote rendering.',
121
121
  placement: 'inline',
122
- document: 'page.md',
122
+ document: 'page.html',
123
123
  steps: 1,
124
124
  slots: [],
125
125
  });
@@ -1,6 +1,6 @@
1
1
  import type { CanvasSource } from '../source.js';
2
2
  import { type BindingId, type BindingResolution, type RawTerminalInput } from '../../keybindings/index.js';
3
- declare const BROWSE_NAV_ACTIONS: readonly ["crtr.browse.quit", "crtr.browse.down", "crtr.browse.up", "crtr.browse.first", "crtr.browse.last", "crtr.browse.expand-or-child", "crtr.browse.collapse-or-parent", "crtr.browse.toggle-fold", "crtr.browse.tab-next", "crtr.browse.tab-previous", "crtr.browse.tab.1", "crtr.browse.tab.2", "crtr.browse.tab.3", "crtr.browse.tab.4", "crtr.browse.tab.5", "crtr.browse.close-subtree", "crtr.browse.sort", "crtr.browse.scope", "crtr.browse.profile", "crtr.browse.residents", "crtr.browse.preview", "crtr.browse.search", "crtr.browse.revive-now", "crtr.browse.focus", "crtr.browse.focus-background"];
3
+ declare const BROWSE_NAV_ACTIONS: readonly ["crtr.browse.quit", "crtr.browse.down", "crtr.browse.up", "crtr.browse.first", "crtr.browse.last", "crtr.browse.expand-or-child", "crtr.browse.collapse-or-parent", "crtr.browse.toggle-fold", "crtr.browse.tab-next", "crtr.browse.tab-previous", "crtr.browse.tab.1", "crtr.browse.tab.2", "crtr.browse.tab.3", "crtr.browse.tab.4", "crtr.browse.tab.5", "crtr.browse.close-subtree", "crtr.browse.sort", "crtr.browse.scope", "crtr.browse.profile", "crtr.browse.residents", "crtr.browse.preview", "crtr.browse.search", "crtr.browse.revive-now", "crtr.browse.focus", "crtr.browse.focus-background", "crtr.browse.yank-id"];
4
4
  declare const BROWSE_SEARCH_ACTIONS: readonly ["crtr.browse.search.cancel", "crtr.browse.search.commit", "crtr.browse.search.delete", "crtr.browse.search.quit"];
5
5
  export declare function resolveBrowseNavAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_NAV_ACTIONS)[number] | null;
6
6
  export declare function resolveBrowseSearchAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_SEARCH_ACTIONS)[number] | null;