@north-light/crouter 0.3.192 → 0.3.193

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/api/client.d.ts +31 -10
  2. package/dist/api/client.js +83 -24
  3. package/dist/api/dto/bash-jobs.d.ts +22 -0
  4. package/dist/api/dto/bash-jobs.js +9 -0
  5. package/dist/api/dto/human.d.ts +3 -4
  6. package/dist/api/dto/inbox.d.ts +106 -65
  7. package/dist/api/dto/inbox.js +2 -8
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -0
  10. package/dist/api/routes.d.ts +7 -0
  11. package/dist/api/routes.js +7 -0
  12. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
  15. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
  17. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
  18. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  19. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  20. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  21. package/dist/clients/attach/input/overlay-owner.js +1 -1
  22. package/dist/clients/attach/overlays/auth.js +34 -8
  23. package/dist/clients/attach/overlays/pickers.d.ts +5 -8
  24. package/dist/clients/attach/overlays/pickers.js +37 -43
  25. package/dist/clients/attach/viewer.js +643 -620
  26. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
  27. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
  28. package/dist/clients/inbox/controller.d.ts +2 -3
  29. package/dist/clients/inbox/controller.js +41 -42
  30. package/dist/clients/inbox/page-adapter.d.ts +33 -0
  31. package/dist/clients/inbox/page-adapter.js +54 -0
  32. package/dist/clients/inbox/resolve.d.ts +2 -2
  33. package/dist/clients/inbox/resolve.js +1 -1
  34. package/dist/clients/inbox/review-visibility.d.ts +1 -1
  35. package/dist/clients/inbox/review-visibility.js +1 -1
  36. package/dist/clients/inbox/tui/ansi.d.ts +1 -1
  37. package/dist/clients/inbox/tui/ansi.js +2 -2
  38. package/dist/clients/inbox/tui/input.d.ts +1 -3
  39. package/dist/clients/inbox/tui/input.js +242 -553
  40. package/dist/clients/inbox/tui/panel.d.ts +3 -1
  41. package/dist/clients/inbox/tui/panel.js +59 -187
  42. package/dist/clients/inbox/tui/render.d.ts +0 -25
  43. package/dist/clients/inbox/tui/render.js +165 -416
  44. package/dist/clients/inbox/tui/slots.d.ts +32 -0
  45. package/dist/clients/inbox/tui/slots.js +156 -0
  46. package/dist/clients/inbox/tui/types.d.ts +25 -13
  47. package/dist/clients/inbox/tui.js +5 -3
  48. package/dist/commands/attention.js +2 -2
  49. package/dist/commands/human/doc.js +66 -20
  50. package/dist/commands/human/prompts.d.ts +3 -3
  51. package/dist/commands/human/prompts.js +158 -112
  52. package/dist/commands/human/queue.d.ts +0 -1
  53. package/dist/commands/human/queue.js +34 -94
  54. package/dist/commands/human/shared.d.ts +10 -4
  55. package/dist/commands/human/shared.js +62 -13
  56. package/dist/commands/human.js +8 -4
  57. package/dist/commands/memory/read.js +13 -5
  58. package/dist/commands/node/bash.js +15 -28
  59. package/dist/commands/surface/node/focus.js +1 -1
  60. package/dist/core/__tests__/human-deliver.test.js +40 -46
  61. package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
  62. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
  63. package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
  64. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
  65. package/dist/core/auth-interaction.d.ts +2 -2
  66. package/dist/core/bash-jobs.d.ts +32 -0
  67. package/dist/core/bash-jobs.js +72 -1
  68. package/dist/core/canvas/__tests__/attention.test.js +16 -10
  69. package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
  70. package/dist/core/canvas/attention.js +1 -1
  71. package/dist/core/canvas/browse/app.js +1 -1
  72. package/dist/core/canvas/nav-model.d.ts +1 -1
  73. package/dist/core/canvas/nav-model.js +1 -1
  74. package/dist/core/canvas/remote-canvas-source.js +1 -1
  75. package/dist/core/config.js +4 -1
  76. package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
  77. package/dist/core/human/__tests__/page-render.test.js +49 -0
  78. package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
  79. package/dist/core/human/__tests__/page-tickets.test.js +112 -0
  80. package/dist/core/human/__tests__/page.test.d.ts +1 -0
  81. package/dist/core/human/__tests__/page.test.js +139 -0
  82. package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
  83. package/dist/core/human/claim.js +2 -2
  84. package/dist/core/human/convention.d.ts +4 -4
  85. package/dist/core/human/convention.js +6 -13
  86. package/dist/core/human/markdown-html.d.ts +8 -0
  87. package/dist/core/human/markdown-html.js +581 -0
  88. package/dist/core/human/page-catalog.d.ts +6 -0
  89. package/dist/core/human/page-catalog.js +43 -0
  90. package/dist/core/human/page-render.d.ts +3 -0
  91. package/dist/core/human/page-render.js +131 -0
  92. package/dist/core/human/page-schema.d.ts +312 -0
  93. package/dist/core/human/page-schema.js +291 -0
  94. package/dist/core/human/page-synth.d.ts +9 -0
  95. package/dist/core/human/page-synth.js +17 -0
  96. package/dist/core/human/page.d.ts +22 -0
  97. package/dist/core/human/page.js +427 -0
  98. package/dist/core/human/review-schema.d.ts +19 -0
  99. package/dist/core/human/review-schema.js +34 -0
  100. package/dist/core/human/scan.d.ts +12 -3
  101. package/dist/core/human/scan.js +69 -26
  102. package/dist/core/human/summary.d.ts +5 -6
  103. package/dist/core/human/summary.js +54 -37
  104. package/dist/core/human/tickets.d.ts +61 -11
  105. package/dist/core/human/tickets.js +100 -57
  106. package/dist/core/human/types.d.ts +21 -66
  107. package/dist/core/human/types.js +1 -5
  108. package/dist/core/preview-registry.js +0 -1
  109. package/dist/core/review/ticket-filter.d.ts +1 -1
  110. package/dist/core/review/ticket-filter.js +2 -8
  111. package/dist/core/runtime/broker/read-ops.js +3 -0
  112. package/dist/core/runtime/broker.js +19 -10
  113. package/dist/core/runtime/package-health.js +1 -1
  114. package/dist/core/termrender/termrender.d.ts +1 -1
  115. package/dist/core/termrender/termrender.js +2 -2
  116. package/dist/core/tui/page-host.d.ts +1 -1
  117. package/dist/core/tui/page-host.js +2 -2
  118. package/dist/core/user-settings.d.ts +19 -0
  119. package/dist/core/user-settings.js +22 -0
  120. package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
  121. package/dist/daemon/api/handlers/bash-jobs.js +89 -0
  122. package/dist/daemon/api/handlers/human.js +16 -14
  123. package/dist/daemon/api/handlers/inbox.js +311 -227
  124. package/dist/daemon/api/router.d.ts +5 -1
  125. package/dist/daemon/api/router.js +5 -0
  126. package/dist/daemon/api/server.js +2 -0
  127. package/dist/daemon/cron-run.js +23 -10
  128. package/dist/daemon/human/finish.d.ts +8 -3
  129. package/dist/daemon/human/finish.js +35 -15
  130. package/dist/pages/bundle.css +1 -0
  131. package/dist/pages/bundle.js +1347 -0
  132. package/dist/pages/comments.d.ts +34 -0
  133. package/dist/pages/comments.js +72 -0
  134. package/dist/pages/elements/cards.d.ts +17 -0
  135. package/dist/pages/elements/cards.js +785 -0
  136. package/dist/pages/elements/chart.d.ts +1 -0
  137. package/dist/pages/elements/chart.js +648 -0
  138. package/dist/pages/elements/options.d.ts +21 -0
  139. package/dist/pages/elements/options.js +604 -0
  140. package/dist/pages/elements/pages.d.ts +24 -0
  141. package/dist/pages/elements/pages.js +355 -0
  142. package/dist/pages/elements/slot.d.ts +21 -0
  143. package/dist/pages/elements/slot.js +105 -0
  144. package/dist/pages/elements/table.d.ts +19 -0
  145. package/dist/pages/elements/table.js +906 -0
  146. package/dist/pages/elements/text.d.ts +25 -0
  147. package/dist/pages/elements/text.js +951 -0
  148. package/dist/pages/entry.d.ts +8 -0
  149. package/dist/pages/entry.js +8 -0
  150. package/dist/pages/host.d.ts +132 -0
  151. package/dist/pages/host.js +149 -0
  152. package/dist/pages/register.d.ts +2 -0
  153. package/dist/pages/register.js +3 -0
  154. package/dist/pages/slot-config.d.ts +36 -0
  155. package/dist/pages/slot-config.js +50 -0
  156. package/dist/pages/types.d.ts +98 -0
  157. package/dist/pages/types.js +23 -0
  158. package/dist/types.d.ts +4 -0
  159. package/dist/types.js +2 -0
  160. package/package.json +8 -8
  161. package/runtime.lock.json +156 -101
  162. package/dist/clients/inbox/deck-adapter.d.ts +0 -29
  163. package/dist/clients/inbox/deck-adapter.js +0 -63
  164. package/dist/core/human/deck-factories.d.ts +0 -8
  165. package/dist/core/human/deck-factories.js +0 -15
  166. package/dist/core/human/deck-schema.d.ts +0 -70
  167. package/dist/core/human/deck-schema.js +0 -92
@@ -6,17 +6,20 @@
6
6
  import { defineLeaf } from '../../core/command.js';
7
7
  import { InputError } from '../../core/io.js';
8
8
  import { cliClient } from '../api-client.js';
9
- import { dirname, isAbsolute, join } from 'node:path';
9
+ import { extname, join } from 'node:path';
10
10
  import { randomBytes } from 'node:crypto';
11
+ import { rmSync } from 'node:fs';
12
+ import { readConfig } from '../../core/config.js';
11
13
  import { tmuxServerReachable } from '../../core/spawn.js';
12
14
  import { markStopSignal } from '../../core/runtime/stop-signals.js';
13
- import { validateDeck, resolveDeckBodyPaths } from '../../core/human/deck-schema.js';
14
- import { notifyDeck } from '../../core/human/deck-factories.js';
15
15
  import { atomicWriteJson } from '../../core/human/convention.js';
16
16
  import { ticketDir } from '../../core/human/root.js';
17
- import { submitDeck } from '../../core/human/tickets.js';
17
+ import { preparePage, publishPage } from '../../core/human/tickets.js';
18
+ import { synthesizedPageDocument, writeSynthesizedPage } from '../../core/human/page-synth.js';
19
+ import { PageAuthoringError } from '../../core/human/page.js';
18
20
  import { display } from '../../core/termrender/display.js';
19
- import { BODY_PATH_CONTRACT, DECK_SCHEMA_HINT, resolveMaxPanes } from './shared.js';
21
+ import { pageHelpText, resolveMaxPanes } from './shared.js';
22
+ const PAGE_HELP = pageHelpText();
20
23
  /** The asking node's id, or null when run from a bare shell (no parent to route to). */
21
24
  function askingNode() {
22
25
  return process.env['CRTR_NODE_ID'] ?? null;
@@ -25,7 +28,7 @@ function askingNode() {
25
28
  * directory, never ticket data; `nodeId` carries the originating node so a
26
29
  * consumer can relate a ticket back to whatever that node belongs to, and
27
30
  * `sessionName` is a presentation label only. Review tickets already stamp
28
- * `nodeId` the same way (`core/review/realize.ts`), so decks match them.
31
+ * `nodeId` the same way (`core/review/realize.ts`), so pages match them.
29
32
  * `nodeId` is stamped even when the daemon is unreachable — losing the label is
30
33
  * survivable, losing machine attribution is not. */
31
34
  export async function sourceStamp() {
@@ -60,49 +63,55 @@ function requireSubtitle(subtitle, field = 'subtitle') {
60
63
  next: 'Write one plain-English sentence stating the decision, recommendation or status, and stakes.',
61
64
  });
62
65
  }
63
- function validateTitledDeck(input, deckDirectory) {
64
- const title = typeof input === 'object' && input !== null
65
- ? input.title
66
- : undefined;
67
- if (typeof title !== 'string' || title.trim() === '') {
68
- throw new Error('deck title is required');
69
- }
70
- const deck = validateDeck(input);
71
- if (deckDirectory === undefined)
72
- return deck;
73
- for (const interaction of deck.interactions) {
74
- if (interaction.bodyPath !== undefined && isAbsolute(interaction.bodyPath)) {
75
- throw new Error('bodyPath must be relative to the deck JSON; absolute paths are unsupported');
76
- }
66
+ function pageDialect(sourceFile) {
67
+ const extension = extname(sourceFile).toLowerCase();
68
+ if (extension === '.md')
69
+ return 'md';
70
+ if (extension === '.html' || extension === '.htm')
71
+ return 'html';
72
+ throw new InputError({
73
+ error: 'invalid_page_dialect',
74
+ message: `--page must point to a .md, .html, or .htm file; received "${sourceFile}"`,
75
+ field: 'page',
76
+ next: 'Use a Markdown page (.md) or an HTML page (.html or .htm).',
77
+ });
78
+ }
79
+ function pageProductKinds(dialect) {
80
+ const config = readConfig('user');
81
+ if (dialect === 'html' && !config.html_pages) {
82
+ throw new InputError({
83
+ error: 'page_dialect_disabled',
84
+ message: 'html pages are not enabled in this home; md is the dialect to use',
85
+ field: 'page',
86
+ next: 'Use a .md page, or enable html_pages in the user scope config before submitting HTML.',
87
+ });
77
88
  }
78
- return resolveDeckBodyPaths(deck, deckDirectory);
89
+ return config.page_components;
79
90
  }
80
- function buildHumanAskDeck(title, subtitle, question) {
81
- return validateTitledDeck({
82
- title,
83
- interactions: [
84
- {
85
- id: 'question',
86
- title,
87
- subtitle,
88
- body: question,
89
- options: [],
90
- allowFreetext: true,
91
- freetextLabel: 'Your answer',
92
- },
93
- ],
91
+ function requireNotifyTitle(title) {
92
+ if (title !== undefined && title.trim() !== '')
93
+ return title.trim();
94
+ throw new InputError({
95
+ error: 'missing_parameter',
96
+ message: 'notify shorthand requires a non-empty title',
97
+ field: 'title',
98
+ next: 'Retry with a positional title, --subtitle, and optional --body, or submit a page with --page PATH.',
94
99
  });
95
100
  }
96
- /** Crouter authors decision decks by default. Persist the explicit kind so
97
- * consumers can rely on the ticket store's strict interaction-kind contract. */
98
- function canonicalizeAskKinds(deck) {
99
- return {
100
- ...deck,
101
- interactions: deck.interactions.map((interaction) => ({
102
- ...interaction,
103
- kind: interaction.kind ?? 'decision',
104
- })),
105
- };
101
+ function submitAuthoredPage(submit) {
102
+ try {
103
+ return submit();
104
+ }
105
+ catch (error) {
106
+ if (!(error instanceof PageAuthoringError))
107
+ throw error;
108
+ throw new InputError({
109
+ error: 'invalid_page',
110
+ message: error.message,
111
+ field: 'page',
112
+ next: 'Fix the authored page, then pre-check it with `crtr human doc check <path>` before submitting again.',
113
+ });
114
+ }
106
115
  }
107
116
  /** The non-blocking status peek returned by ask/review. How the human reaches
108
117
  * their inbox is theirs, not the agent's — naming a shortcut here only teaches
@@ -121,83 +130,85 @@ export const humanAsk = defineLeaf({
121
130
  help: {
122
131
  name: 'human ask',
123
132
  summary: "submit a decision ticket to crouter's inbox; returns a job handle immediately.",
124
- guide: 'Write every interaction as a self-contained decision brief for a supervisor who has not followed this agent or task: use the title to identify the inbox topic; write a one-sentence subtitle stating the decision, your recommendation, and what it changes; then give the minimum plain-language context and tradeoffs needed to decide in the body. Never coin a term, code name, or shorthand for a system, mechanism, or state, and expand any project-internal name you do use: name the thing plainly and say what it does. Invented vocabulary is not compression — the reader has to decode it before they can decide. Put supporting evidence, chronology, and technical detail under `## Details` so the person can answer without logs, screenshots, or prior conversation. For a simple open-text question, supply the short topic with --title, the required sentence with --subtitle, and pipe the markdown body on stdin. Use --context-file for a fully structured deck; its deck title and every interaction title and subtitle are required. The deck body is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `crtr human doc check` before authoring one. Ask only what materially changes your next step, and offer genuine alternatives as `options`.',
133
+ guide: [
134
+ 'Write a self-contained decision brief for a supervisor who has not followed the task: title names the topic; subtitle states the decision, your recommendation, and stakes; the body gives only the context and tradeoffs needed to decide. Put supporting evidence under `## Details`. Ask only what changes your next step, and offer genuine alternatives as `options`.',
135
+ PAGE_HELP.authoring,
136
+ ].join('\n\n'),
125
137
  params: [
126
- { kind: 'flag', name: 'title', type: 'string', required: false, constraint: 'Required with stdin: a short ≤~4-word inbox topic. Mutually exclusive with --context-file, whose JSON carries its own required title.' },
127
- { kind: 'flag', name: 'subtitle', type: 'string', required: false, constraint: 'Required with stdin: one plain-English sentence stating the decision, recommendation, and stakes. Mutually exclusive with --context-file, whose JSON requires every interaction subtitle.' },
128
- { kind: 'stdin', name: 'question', required: false, constraint: "Simple open-text ask body. Pipe markdown from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Requires --title and --subtitle; mutually exclusive with --context-file." },
129
- { kind: 'context-file', name: 'deck', required: false, constraint: `Strict structured deck JSON with a required deck title and non-empty subtitle on every interaction. ${BODY_PATH_CONTRACT}. Use this for multiple interactions, options, or other deck structure. Mutually exclusive with --title, --subtitle, and the question shorthand.`, shape: DECK_SCHEMA_HINT },
138
+ { kind: 'flag', name: 'title', type: 'string', required: false, constraint: 'Required with stdin shorthand: a short ≤~4-word inbox topic. Mutually exclusive with --page.' },
139
+ { kind: 'flag', name: 'subtitle', type: 'string', required: false, constraint: 'Required with stdin shorthand: one plain-English sentence stating the decision, recommendation, and stakes. Mutually exclusive with --page.' },
140
+ { kind: 'stdin', name: 'question', required: false, constraint: "Simple open-text ask body. Pipe markdown from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Requires --title and --subtitle; mutually exclusive with --page." },
141
+ { kind: 'flag', name: 'page', type: 'path', required: false, constraint: `${PAGE_HELP.pageHint} Mutually exclusive with shorthand.` },
130
142
  ],
131
143
  output: [
132
144
  { name: 'job_id', type: 'string', required: true, constraint: 'Node id of this human interaction. Its answer is pushed to your inbox when the human responds.' },
133
- { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding deck.json/run.json/response.json.' },
145
+ { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding page.md or page.html, page.json, run.json, and response.json.' },
134
146
  { name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking status peek; the answer arrives via your inbox.' },
135
147
  ],
136
148
  outputKind: 'object',
137
149
  effects: [
138
- 'Creates a kind:"human" node under you and enqueues a decision deck in the human inbox.',
150
+ 'Creates a kind:"human" node under you and enqueues a page in the human inbox.',
139
151
  'Returns immediately; nothing opens on screen. The human answers from their inbox on their own time.',
140
152
  ],
141
153
  },
142
- run: async (input, context) => {
143
- // Optional stdin reaches the leaf as an empty string when omitted, including
144
- // when --context-file is supplied. Keep the raw non-empty bytes untouched.
154
+ run: async (input) => {
155
+ // Optional stdin reaches the leaf as an empty string when omitted. Keep the
156
+ // raw non-empty bytes untouched so shorthand callers retain their body.
145
157
  const titleRaw = input['title'];
146
158
  const title = typeof titleRaw === 'string' && titleRaw.trim() !== '' ? titleRaw.trim() : undefined;
147
159
  const questionRaw = input['question'];
148
160
  const question = typeof questionRaw === 'string' && questionRaw !== '' ? questionRaw : undefined;
149
161
  const subtitleRaw = input['subtitle'];
150
162
  const subtitle = typeof subtitleRaw === 'string' && subtitleRaw.trim() !== '' ? subtitleRaw.trim() : undefined;
151
- const deckInput = input['deck'];
152
- const deckPath = context?.contextFilePaths.get('deck');
153
- if (deckInput !== undefined && (question !== undefined || titleRaw !== undefined || subtitleRaw !== undefined)) {
163
+ const pageInput = input['page'];
164
+ if (pageInput !== undefined && (question !== undefined || titleRaw !== undefined || subtitleRaw !== undefined)) {
154
165
  throw new InputError({
155
166
  error: 'bad_invocation',
156
- message: 'use --title and --subtitle with stdin question text, or --context-file, not both',
157
- next: 'Retry with --title, --subtitle, and a piped question body, or with --context-file alone.',
167
+ message: 'use --title and --subtitle with stdin question text, or --page, not both',
168
+ next: 'Retry with --title, --subtitle, and a piped question body, or with --page PATH alone.',
158
169
  });
159
170
  }
160
- if (question === undefined && deckInput === undefined) {
171
+ if (question === undefined && pageInput === undefined) {
161
172
  throw new InputError({
162
173
  error: 'missing_parameter',
163
- message: 'human ask needs stdin question text or --context-file deck.json',
164
- next: 'Pipe a question body with --title and --subtitle, or pass --context-file for a structured deck.',
174
+ message: 'human ask needs stdin question text or --page PATH',
175
+ next: 'Pipe a question body with --title and --subtitle, or pass --page PATH for an authored page.',
165
176
  });
166
177
  }
167
- const simpleAsk = question === undefined
168
- ? undefined
169
- : { title: requireAskTitle(title), subtitle: requireSubtitle(subtitle), question };
170
- let deck;
178
+ const dialect = pageDialect(typeof pageInput === 'string' ? pageInput : 'page.md');
179
+ const productKinds = pageProductKinds(dialect);
180
+ let tempDir;
181
+ let sourceFile;
182
+ if (pageInput !== undefined) {
183
+ sourceFile = pageInput;
184
+ }
185
+ else {
186
+ const simpleAsk = { title: requireAskTitle(title), subtitle: requireSubtitle(subtitle), question: question };
187
+ const synthesized = writeSynthesizedPage(synthesizedPageDocument(simpleAsk.title, simpleAsk.subtitle, simpleAsk.question, true));
188
+ tempDir = synthesized.dir;
189
+ sourceFile = synthesized.path;
190
+ }
171
191
  try {
172
- if (simpleAsk !== undefined) {
173
- deck = buildHumanAskDeck(simpleAsk.title, simpleAsk.subtitle, simpleAsk.question);
174
- }
175
- else {
176
- deck = validateTitledDeck(deckInput, deckPath === undefined ? undefined : dirname(deckPath));
177
- }
178
- deck = canonicalizeAskKinds(deck);
192
+ const source = await sourceStamp();
193
+ // The page is validated before the bridge node exists: a rejected page
194
+ // must create nothing, because a bridge node born for a ticket that never
195
+ // got published is a live terminal node no answer can ever finish.
196
+ const prepared = submitAuthoredPage(() => preparePage({ sourceFile, dialect, source, productKinds }));
197
+ const cwd = process.cwd();
198
+ const jobId = (await cliClient().createHumanBridge({ kind: 'human', parent: askingNode(), cwd, name: 'human-ask' })).node_id;
199
+ const idir = ticketDir(jobId);
200
+ const rc = { mode: 'ask', job_id: jobId };
201
+ atomicWriteJson(join(idir, 'run.json'), rc);
202
+ const { dir } = publishPage(ticketDir(jobId), prepared);
203
+ const originNodeId = askingNode();
204
+ if (originNodeId !== null && originNodeId !== '')
205
+ markStopSignal(originNodeId, 'askedHuman');
206
+ return { job_id: jobId, dir, follow_up: queuedFollowUp() };
179
207
  }
180
- catch (e) {
181
- throw new InputError({
182
- error: 'deck_invalid',
183
- message: String(e),
184
- field: 'deck',
185
- next: DECK_SCHEMA_HINT,
186
- });
208
+ finally {
209
+ if (tempDir !== undefined)
210
+ rmSync(tempDir, { recursive: true, force: true });
187
211
  }
188
- // Crouter owns this provenance. Never synthesize labels from deck content:
189
- // an unavailable daemon leaves only machine attribution for routing.
190
- deck = { ...deck, source: await sourceStamp() };
191
- const cwd = process.cwd();
192
- const jobId = (await cliClient().createHumanBridge({ kind: 'human', parent: askingNode(), cwd, name: 'human-ask' })).node_id;
193
- const idir = ticketDir(jobId);
194
- const rc = { mode: 'ask', job_id: jobId };
195
- atomicWriteJson(join(idir, 'run.json'), rc);
196
- const { dir } = submitDeck({ dir: ticketDir(jobId), deck });
197
- const originNodeId = askingNode();
198
- if (originNodeId !== null && originNodeId !== '')
199
- markStopSignal(originNodeId, 'askedHuman');
200
- return { job_id: jobId, dir, follow_up: queuedFollowUp() };
201
212
  },
202
213
  });
203
214
  // ---------------------------------------------------------------------------
@@ -210,33 +221,68 @@ export const humanNotify = defineLeaf({
210
221
  help: {
211
222
  name: 'human notify',
212
223
  summary: 'enqueue a fire-and-forget acknowledgement in the human inbox; creates no job',
213
- guide: 'State what happened in plain words — no coined terms or internal shorthand for a system or concept; name it and say what it does. The body is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `crtr human doc check` before authoring one.',
224
+ guide: [
225
+ 'State what happened in plain words. Shorthand bodies are directive-flavored Markdown; validate authored pages with `crtr human doc check`.',
226
+ PAGE_HELP.authoring,
227
+ ].join('\n\n'),
214
228
  params: [
215
- { kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'The notification headline.' },
216
- { kind: 'flag', name: 'subtitle', type: 'string', required: true, constraint: 'One plain-English sentence stating the notification status and stakes.' },
217
- { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Optional markdown body.' },
229
+ { kind: 'positional', name: 'title', type: 'string', required: false, constraint: 'Notification headline for shorthand; required with --subtitle when --page is omitted.' },
230
+ { kind: 'flag', name: 'subtitle', type: 'string', required: false, constraint: 'One plain-English sentence stating the notification status and stakes; required when --page is omitted.' },
231
+ { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Optional markdown body for shorthand; mutually exclusive with --page.' },
232
+ { kind: 'flag', name: 'page', type: 'path', required: false, constraint: `${PAGE_HELP.pageHint} Mutually exclusive with --body.` },
218
233
  ],
219
234
  output: [
220
- { name: 'queued', type: 'boolean', required: true, constraint: 'Always true — the notify deck is durable inbox state the human acknowledges from the inbox.' },
221
- { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding deck.json.' },
235
+ { name: 'queued', type: 'boolean', required: true, constraint: 'Always true — the notify page is durable inbox state the human acknowledges from the inbox.' },
236
+ { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding page.md or page.html, page.json, and run.json.' },
222
237
  ],
223
238
  outputKind: 'object',
224
239
  effects: [
225
- 'Enqueues a durable notify deck in the human inbox. Creates no node; nothing opens on screen.',
240
+ 'Enqueues a durable display page in the human inbox. Creates no node; nothing opens on screen.',
226
241
  ],
227
242
  },
228
243
  run: async (input) => {
229
- const title = input['title'];
230
- const subtitle = requireSubtitle(input['subtitle']);
244
+ const titleRaw = input['title'];
245
+ const title = typeof titleRaw === 'string' && titleRaw.trim() !== '' ? titleRaw.trim() : undefined;
246
+ const subtitleRaw = input['subtitle'];
247
+ const subtitle = typeof subtitleRaw === 'string' && subtitleRaw.trim() !== '' ? subtitleRaw.trim() : undefined;
231
248
  const body = input['body'];
232
- let deck = notifyDeck(title, { subtitle, ...(body !== undefined ? { body } : {}) });
233
- deck = { ...deck, source: await sourceStamp() };
234
- const id = `nfy-${randomBytes(4).toString('hex')}`;
235
- const idir = ticketDir(id);
236
- const rc = { mode: 'notify' };
237
- atomicWriteJson(join(idir, 'run.json'), rc);
238
- const { dir } = submitDeck({ dir: ticketDir(id), deck });
239
- return { queued: true, dir };
249
+ const pageInput = input['page'];
250
+ if (pageInput !== undefined && body !== undefined) {
251
+ throw new InputError({
252
+ error: 'bad_invocation',
253
+ message: 'use --page or --body, not both',
254
+ next: 'Retry with --page PATH alone for an authored page, or use the title, --subtitle, and --body shorthand.',
255
+ });
256
+ }
257
+ const dialect = pageDialect(typeof pageInput === 'string' ? pageInput : 'page.md');
258
+ const productKinds = pageProductKinds(dialect);
259
+ let tempDir;
260
+ let sourceFile;
261
+ if (pageInput !== undefined) {
262
+ sourceFile = pageInput;
263
+ }
264
+ else {
265
+ const shorthandTitle = requireNotifyTitle(title);
266
+ const shorthandSubtitle = requireSubtitle(subtitle);
267
+ const synthesized = writeSynthesizedPage(synthesizedPageDocument(shorthandTitle, shorthandSubtitle, body ?? '', false));
268
+ tempDir = synthesized.dir;
269
+ sourceFile = synthesized.path;
270
+ }
271
+ try {
272
+ const source = await sourceStamp();
273
+ // Validate before the ticket directory exists, so a rejected page leaves
274
+ // no half-built ticket behind.
275
+ const prepared = submitAuthoredPage(() => preparePage({ sourceFile, dialect, source, productKinds }));
276
+ const id = `nfy-${randomBytes(4).toString('hex')}`;
277
+ const rc = { mode: 'notify' };
278
+ atomicWriteJson(join(ticketDir(id), 'run.json'), rc);
279
+ const { dir } = publishPage(ticketDir(id), prepared);
280
+ return { queued: true, dir };
281
+ }
282
+ finally {
283
+ if (tempDir !== undefined)
284
+ rmSync(tempDir, { recursive: true, force: true });
285
+ }
240
286
  },
241
287
  });
242
288
  // ---------------------------------------------------------------------------
@@ -251,7 +297,7 @@ export const humanShow = defineLeaf({
251
297
  help: {
252
298
  name: 'human show',
253
299
  summary: "put a file live on screen in a tmux pane with crouter's renderer",
254
- guide: 'The pane always watches the file and live-updates on every save — a displayed doc is a live view by definition, so point it at a file something keeps rewriting (a status board, a running summary) and it stays current. The file is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `crtr human doc render` before authoring one.',
300
+ guide: 'The pane always watches the file and live-updates on every save — a displayed doc is a live view by definition, so point it at a file something keeps rewriting (a status board, a running summary) and it stays current. The file is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `crtr human doc render` before authoring one. To put a rich document in front of the human as a durable inbox page, use `crtr human notify --page PATH`.',
255
301
  params: [
256
302
  { kind: 'positional', name: 'path', type: 'path', required: true, constraint: 'Path to the file to render.' },
257
303
  { kind: 'flag', name: 'window', type: 'enum', choices: ['auto', 'split', 'new'], required: false, default: 'auto', constraint: 'Placement. Default auto.' },
@@ -1,4 +1,3 @@
1
1
  export declare const humanList: import("../../core/command.js").LeafDef;
2
- export declare const humanDeck: import("../../core/command.js").LeafDef;
3
2
  export declare const humanResolve: import("../../core/command.js").LeafDef;
4
3
  export declare const humanCancel: import("../../core/command.js").LeafDef;
@@ -1,16 +1,15 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { basename } from 'node:path';
2
+ import { basename, join } from 'node:path';
3
3
  import { ApiError } from '../../api/index.js';
4
4
  import { defineLeaf } from '../../core/command.js';
5
5
  import { ticketDir } from '../../core/human/root.js';
6
6
  import { scanInbox } from '../../core/human/scan.js';
7
- import { parseDeck } from '../../core/human/deck-schema.js';
8
- import { deckPath, isResolved, reviewPath } from '../../core/human/convention.js';
9
- import { readJsonOrNull } from '../../core/fs-utils.js';
7
+ import { pageManifestPath, isResolved, reviewPath } from '../../core/human/convention.js';
10
8
  import { InputError } from '../../core/io.js';
11
9
  import { paginate } from '../../core/pagination.js';
12
10
  import { cliClient, rethrowAsCliError } from '../api-client.js';
13
11
  import { filterVisibleReviewTickets } from '../../clients/inbox/review-visibility.js';
12
+ import { readJsonOrNull } from '../../core/fs-utils.js';
14
13
  function toRef(n) {
15
14
  return { node_id: n.node_id, name: n.name, cwd: n.cwd, parent: n.parent, status: n.status };
16
15
  }
@@ -60,29 +59,36 @@ function askingNodeFor(jobId, byId) {
60
59
  return byId.get(bridge.parent) ?? bridge;
61
60
  return bridge;
62
61
  }
62
+ /** `notify` enqueues a durable page with no bridge node at all — its run.json
63
+ * is the manifest-based truth that distinguishes that intentional, workerless
64
+ * ticket from an `ask`/`review` ticket whose bridge node was reaped. */
65
+ function isWorkerlessTicket(dir) {
66
+ return readJsonOrNull(join(dir, 'run.json'))?.mode === 'notify';
67
+ }
63
68
  function summarizeTicket(summary, nodes, byId) {
64
69
  const jobId = basename(summary.dir);
65
- // Every crouter ticket lives directly in its bridge node directory. A stale
66
- // ticket whose node was reaped is intentionally absent from the canvas view.
67
- if (!byId.has(jobId))
70
+ const workerless = isWorkerlessTicket(summary.dir);
71
+ // A ticket that expects a bridge node but has none is a stale ticket whose
72
+ // node was reaped, intentionally absent from the canvas view. A workerless
73
+ // notify ticket never had one, so it renders with its node-derived fields
74
+ // omitted rather than being dropped as an orphan.
75
+ if (!workerless && !byId.has(jobId))
68
76
  return null;
69
- const asking = askingNodeFor(jobId, byId);
77
+ const asking = workerless ? undefined : askingNodeFor(jobId, byId);
70
78
  const conversation = resolveConversation(asking, nodes);
71
- const interactionCount = summary.kind === 'deck' ? readJsonOrNull(deckPath(summary.dir))?.interactions.length : undefined;
72
79
  const sourceLabel = readableSourceLabel(summary.source);
73
80
  return {
74
81
  id: summary.id,
75
- job_id: jobId,
82
+ ...(workerless ? {} : { job_id: jobId }),
76
83
  dir: summary.dir,
77
84
  title: summary.title,
78
85
  subtitle: summary.subtitle,
79
- kind: summary.kind === 'deck' ? summary.interactionKind ?? null : 'review',
86
+ kind: summary.kind,
80
87
  blocked_since: summary.blockedSince,
81
88
  ...(summary.claim !== undefined ? { claim_owner: summary.claim.owner } : {}),
82
89
  ...(sourceLabel !== undefined ? { source_label: sourceLabel } : {}),
83
90
  ...(asking !== undefined ? { asking_node_id: asking.node_id, asking_node_name: asking.name, cwd: asking.cwd } : {}),
84
91
  ...(conversation !== undefined ? { conversation_id: conversation.node_id, conversation_title: conversation.name } : {}),
85
- ...(interactionCount !== undefined ? { interaction_count: interactionCount } : {}),
86
92
  };
87
93
  }
88
94
  function interactionDirForJob(jobId, byId) {
@@ -112,7 +118,7 @@ export const humanList = defineLeaf({
112
118
  { kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: "Opaque token from a previous response's next_cursor. Omit on first call." },
113
119
  ],
114
120
  output: [
115
- { name: 'items', type: 'object[]', required: true, constraint: 'Each: {id, dir, title, subtitle, kind, blocked_since}; subtitle is the required authored one-sentence summary. Enriched when derivable with claim_owner, source_label, asking_node_id/name, conversation_id/title, interaction_count. Oldest first.' },
121
+ { name: 'items', type: 'object[]', required: true, constraint: 'Each: {id, dir, title, subtitle, kind, blocked_since}; subtitle is the required authored one-sentence summary. Enriched when derivable with claim_owner, source_label, asking_node_id/name, conversation_id/title. Oldest first.' },
116
122
  { name: 'next_cursor', type: 'string | null', required: true, constraint: 'Pass on the next call to continue. null means no more items.' },
117
123
  { name: 'total', type: 'integer | null', required: true, constraint: 'Total pending interactions.' },
118
124
  ],
@@ -147,103 +153,37 @@ export const humanList = defineLeaf({
147
153
  },
148
154
  });
149
155
  // ---------------------------------------------------------------------------
150
- // deck detail (read-only)
156
+ // resolve — non-interactive human answer
151
157
  // ---------------------------------------------------------------------------
152
- export const humanDeck = defineLeaf({
153
- name: 'deck',
154
- description: 'read one pending human deck',
155
- whenToUse: 'you need the full questions/options for a pending human interaction',
156
- help: {
157
- name: 'human deck',
158
- summary: 'read full pending deck detail for one human interaction job',
159
- params: [{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'job_id from human ask/review/list.' }],
160
- output: [
161
- { name: 'id', type: 'string', required: true, constraint: 'Interaction job id.' },
162
- { name: 'interactions', type: 'object[]', required: true, constraint: 'Deck interactions with required authored subtitles, bodies, options, and freetext policy.' },
163
- ],
164
- outputKind: 'object',
165
- effects: ['Read-only: reads deck.json from the interaction dir resolved through the canvas node cwd.'],
166
- },
167
- run: async (input) => {
168
- const jobId = input['job_id'];
169
- const nodes = await fetchNodeRefs();
170
- const byId = new Map(nodes.map((n) => [n.node_id, n]));
171
- const { dir } = interactionDirForJob(jobId, byId);
172
- if (isResolved(dir)) {
173
- throw new InputError({ error: 'already_resolved', message: `interaction already resolved: ${jobId}`, field: 'job_id', next: 'Use `crtr human list` for pending interactions.' });
174
- }
175
- let deck;
176
- try {
177
- deck = parseDeck(deckPath(dir));
178
- }
179
- catch {
180
- throw new InputError({ error: 'not_found', message: `no deck for interaction: ${jobId}`, field: 'job_id', next: 'Pass a pending job_id from `crtr human list`.' });
181
- }
182
- const asking = askingNodeFor(jobId, byId);
183
- const conversation = resolveConversation(asking, nodes);
184
- const sourceLabel = readableSourceLabel(deck.source);
185
- const first = deck.interactions[0];
186
- return {
187
- id: jobId,
188
- title: deck.title,
189
- kind: first.kind ?? null,
190
- subtitle: first.subtitle,
191
- blocked_since: deck.source?.blockedSince ?? null,
192
- ...(sourceLabel !== undefined ? { source_label: sourceLabel } : {}),
193
- ...(asking !== undefined ? { asking_node_id: asking.node_id, asking_node_name: asking.name, cwd: asking.cwd } : {}),
194
- ...(conversation !== undefined ? { conversation_id: conversation.node_id, conversation_title: conversation.name } : {}),
195
- interaction_count: deck.interactions.length,
196
- interactions: deck.interactions.map((it) => ({
197
- id: it.id,
198
- kind: it.kind ?? null,
199
- title: it.title,
200
- subtitle: it.subtitle,
201
- ...(it.body !== undefined ? { body: it.body } : {}),
202
- options: it.options.map((o) => ({ id: o.id, label: o.label, ...(o.description !== undefined ? { description: o.description } : {}) })),
203
- allow_freetext: it.allowFreetext ?? false,
204
- ...(it.preAnswered?.selectedOptionId !== undefined ? { default_option_id: it.preAnswered.selectedOptionId } : {}),
205
- })),
206
- };
207
- },
208
- });
158
+ /** Decode the stdin envelope only; the daemon validates every response against the page manifest. */
209
159
  function parseResolveBody(raw) {
210
160
  let parsed;
211
161
  try {
212
162
  parsed = JSON.parse(raw);
213
163
  }
214
164
  catch (e) {
215
- throw new InputError({ error: 'invalid_json', message: `stdin is not valid JSON: ${String(e)}`, field: 'stdin', next: 'Pass {"responses":[{"interaction_id":"..."}]} on stdin.' });
165
+ throw new InputError({ error: 'invalid_json', message: `stdin is not valid JSON: ${String(e)}`, field: 'stdin', next: 'Pass {"responses":{"<slot_id>":{...}}} on stdin.' });
216
166
  }
217
- if (!Array.isArray(parsed.responses)) {
218
- throw new InputError({ error: 'invalid_field', message: 'responses must be an array', field: 'responses', next: 'Pass {"responses":[{"interaction_id":"...","selected_option_ids":["..."]}]}.' });
167
+ const responses = parsed.responses;
168
+ if (typeof responses !== 'object' || responses === null || Array.isArray(responses)) {
169
+ throw new InputError({ error: 'invalid_field', message: 'responses must be an object keyed by slot id', field: 'responses', next: 'Pass {"responses":{"<slot_id>":{...}}}, or {"responses":{}} to acknowledge a page with no response slots.' });
219
170
  }
220
- return parsed.responses.map((r) => {
221
- if (r.interaction_id === undefined || r.interaction_id === '') {
222
- throw new InputError({ error: 'invalid_field', message: 'each response needs interaction_id', field: 'responses', next: 'Include interaction_id for every response.' });
223
- }
224
- return {
225
- id: r.interaction_id,
226
- ...(r.selected_option_ids !== undefined ? { selectedOptionIds: r.selected_option_ids } : {}),
227
- ...(r.selected_option_id !== undefined ? { selectedOptionId: r.selected_option_id } : {}),
228
- ...(r.freetext !== undefined ? { freetext: r.freetext } : {}),
229
- ...(r.option_comments !== undefined ? { optionComments: r.option_comments } : {}),
230
- };
231
- });
171
+ return responses;
232
172
  }
233
173
  export const humanResolve = defineLeaf({
234
174
  name: 'resolve',
235
- description: 'resolve one human deck from stdin answers',
175
+ description: 'resolve one human page from stdin answers',
236
176
  whenToUse: 'an API client or local tool is submitting answers for a pending human interaction.',
237
177
  tier: 'hidden',
238
178
  help: {
239
179
  name: 'human resolve',
240
- summary: 'resolve a pending deck with the given answers and deliver them to the asking node',
180
+ summary: 'resolve a pending page with the given answers and deliver them to the asking node',
241
181
  params: [
242
182
  { kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'job_id from human ask/review/list.' },
243
- { kind: 'stdin', name: 'body', required: true, constraint: 'JSON {responses:[{interaction_id, selected_option_ids?, freetext?}]}.' },
183
+ { kind: 'stdin', name: 'body', required: true, constraint: 'JSON {responses:{<slot_id>:{...}}} — one typed response object per response-bearing slot of the page.' },
244
184
  ],
245
185
  output: [
246
- { name: 'resolved', type: 'boolean', required: true, constraint: 'True when this call answered and delivered the deck to the asking node.' },
186
+ { name: 'resolved', type: 'boolean', required: true, constraint: 'True when this call answered and delivered the page to the asking node.' },
247
187
  { name: 'job_id', type: 'string', required: true, constraint: 'Interaction job id.' },
248
188
  { name: 'reason', type: 'string', required: false, constraint: 'already_resolved or claimed when resolved is false.' },
249
189
  ],
@@ -255,8 +195,8 @@ export const humanResolve = defineLeaf({
255
195
  const nodes = await fetchNodeRefs();
256
196
  const byId = new Map(nodes.map((n) => [n.node_id, n]));
257
197
  const { dir, node } = interactionDirForJob(jobId, byId);
258
- if (!existsSync(deckPath(dir))) {
259
- throw new InputError({ error: 'not_found', message: `no deck for interaction: ${jobId}`, field: 'job_id', next: 'Pass a pending job_id from `crtr human list`.' });
198
+ if (!existsSync(pageManifestPath(dir))) {
199
+ throw new InputError({ error: 'not_found', message: `no page for interaction: ${jobId}`, field: 'job_id', next: 'Pass a pending job_id from `crtr human list`.' });
260
200
  }
261
201
  const responses = parseResolveBody(input['body']);
262
202
  // A published result on a live bridge may be an interrupted delivery. The
@@ -286,7 +226,7 @@ export const humanCancel = defineLeaf({
286
226
  help: {
287
227
  name: 'human cancel',
288
228
  summary: 'retract a pending ask/review you posed — drops it from the human inbox and retires the bridge node. Reach for this the moment a question goes stale (you answered it yourself, the situation changed) so a human is not left resolving a prompt whose answer no longer matters',
289
- guide: 'Pass the job_id returned by `human ask`/`review`. Best-effort and idempotent: if the human already answered, or it was already canceled, it reports canceled:false with reason "already_resolved" and changes nothing. Cancellation races the human\'s submit; whichever wins is authoritative. The agent that posed the deck is almost always the one canceling it, so the caller is never messaged — only OTHER subscribers get a quiet deferred note that no answer is coming.',
229
+ guide: 'Pass the job_id returned by `human ask`/`review`. Best-effort and idempotent: if the human already answered, or it was already canceled, it reports canceled:false with reason "already_resolved" and changes nothing. Cancellation races the human\'s submit; whichever wins is authoritative. The agent that posed the question is almost always the one canceling it, so the caller is never messaged — only OTHER subscribers get a quiet deferred note that no answer is coming.',
290
230
  params: [
291
231
  { kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Node id of the interaction to cancel — the job_id returned by ask/review.' },
292
232
  { kind: 'flag', name: 'reason', type: 'string', required: false, constraint: 'Optional short note delivered to subscribers explaining why it was retracted.' },
@@ -316,7 +256,7 @@ export const humanCancel = defineLeaf({
316
256
  const idir = ticketDir(jobId);
317
257
  // Nothing live to cancel: the bridge is already terminal, or the ticket
318
258
  // never carried a request descriptor.
319
- const hasDescriptor = existsSync(deckPath(idir)) || existsSync(reviewPath(idir));
259
+ const hasDescriptor = existsSync(pageManifestPath(idir)) || existsSync(reviewPath(idir));
320
260
  if (isTerminalBridge(node.status) || !hasDescriptor) {
321
261
  return { canceled: false, job_id: jobId, reason: 'already_resolved' };
322
262
  }
@@ -1,10 +1,16 @@
1
- export declare const BODY_PATH_CONTRACT: string;
2
- export declare const DECK_SCHEMA_HINT: string;
1
+ export interface PageHelpText {
2
+ authoring: string;
3
+ pageHint: string;
4
+ }
5
+ /** Invocation-local page capability and authoring contract for `human -h`.
6
+ * The CLI process rebuilds command help each time, so config changes appear on
7
+ * the next invocation without a parallel documentation surface. */
8
+ export declare function pageHelpText(): PageHelpText;
3
9
  /**
4
10
  * Crouter's private per-interaction record. Reduced to the delivery essentials:
5
11
  * the mode that identifies the ticket and the bridge node id it delivers back to.
6
- * The ticket store owns every public request/result/output path (deck.json,
7
- * response.json, review.json); crouter never records pane ids or output paths.
12
+ * The ticket store owns every public request/result/output path (page.md/page.html,
13
+ * page.json, response.json, review.json); crouter never records pane ids or output paths.
8
14
  *
9
15
  */
10
16
  export interface RunRecord {