@north-light/crouter 0.3.197 → 0.3.198

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/dto/inbox.d.ts +46 -0
  3. package/dist/builtin-memory/00-runtime-base.md +3 -3
  4. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  5. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  7. package/dist/clients/attach/render/chat-view.d.ts +3 -8
  8. package/dist/clients/attach/render/chat-view.js +3 -24
  9. package/dist/clients/attach/render/markdown-source.js +0 -6
  10. package/dist/clients/attach/render/tool-calls.js +16 -2
  11. package/dist/clients/attach/viewer.js +613 -602
  12. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
  13. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
  14. package/dist/clients/inbox/controller.js +4 -4
  15. package/dist/clients/inbox/page-adapter.d.ts +2 -1
  16. package/dist/clients/inbox/tui/render.js +46 -25
  17. package/dist/clients/inbox/tui/slots.d.ts +1 -3
  18. package/dist/clients/inbox/tui/slots.js +8 -37
  19. package/dist/clients/inbox/tui/types.d.ts +4 -3
  20. package/dist/commands/__tests__/human.test.js +160 -23
  21. package/dist/commands/canvas-browse.js +1 -1
  22. package/dist/commands/human/doc.js +21 -74
  23. package/dist/commands/human/html.d.ts +2 -0
  24. package/dist/commands/human/html.js +81 -0
  25. package/dist/commands/human/prompts.d.ts +5 -4
  26. package/dist/commands/human/prompts.js +81 -153
  27. package/dist/commands/human/shared.d.ts +5 -6
  28. package/dist/commands/human/shared.js +13 -61
  29. package/dist/commands/human.js +7 -2
  30. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  31. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  32. package/dist/core/__tests__/human-deliver.test.js +28 -37
  33. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  34. package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
  35. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
  36. package/dist/core/canvas/__tests__/attention.test.js +2 -2
  37. package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
  38. package/dist/core/canvas/browse/app.d.ts +1 -1
  39. package/dist/core/canvas/browse/app.js +21 -3
  40. package/dist/core/canvas/browse/render.d.ts +4 -0
  41. package/dist/core/canvas/browse/render.js +5 -1
  42. package/dist/core/canvas/canvas.d.ts +26 -0
  43. package/dist/core/canvas/canvas.js +55 -0
  44. package/dist/core/command.js +6 -11
  45. package/dist/core/config.js +3 -2
  46. package/dist/core/help.d.ts +3 -0
  47. package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
  48. package/dist/core/human/__tests__/html-markdown.test.js +52 -0
  49. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  50. package/dist/core/human/__tests__/page-catalog.test.js +16 -0
  51. package/dist/core/human/__tests__/page-render.test.js +40 -23
  52. package/dist/core/human/__tests__/page-tickets.test.js +48 -27
  53. package/dist/core/human/__tests__/page.test.js +48 -65
  54. package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
  55. package/dist/core/human/answer-text.d.ts +3 -0
  56. package/dist/core/human/answer-text.js +98 -0
  57. package/dist/core/human/answer.d.ts +51 -0
  58. package/dist/core/human/answer.js +125 -0
  59. package/dist/core/human/component-docs.d.ts +14 -0
  60. package/dist/core/human/component-docs.js +161 -0
  61. package/dist/core/human/html-markdown.d.ts +3 -0
  62. package/dist/core/human/html-markdown.js +288 -0
  63. package/dist/core/human/page-catalog.d.ts +19 -3
  64. package/dist/core/human/page-catalog.js +73 -16
  65. package/dist/core/human/page-render.d.ts +3 -2
  66. package/dist/core/human/page-render.js +19 -45
  67. package/dist/core/human/page-schema.d.ts +108 -8
  68. package/dist/core/human/page-schema.js +88 -31
  69. package/dist/core/human/page-synth.d.ts +2 -4
  70. package/dist/core/human/page-synth.js +23 -7
  71. package/dist/core/human/page.d.ts +14 -7
  72. package/dist/core/human/page.js +178 -90
  73. package/dist/core/human/review-schema.d.ts +1 -0
  74. package/dist/core/human/review-schema.js +1 -1
  75. package/dist/core/human/scan.d.ts +3 -2
  76. package/dist/core/human/scan.js +7 -8
  77. package/dist/core/human/summary.d.ts +4 -7
  78. package/dist/core/human/summary.js +4 -60
  79. package/dist/core/human/tickets.d.ts +10 -9
  80. package/dist/core/human/tickets.js +5 -3
  81. package/dist/core/human/types.d.ts +4 -0
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +1 -0
  84. package/dist/core/review/realize.js +9 -1
  85. package/dist/core/runtime/boot-root.js +6 -1
  86. package/dist/core/runtime/broker.js +8 -6
  87. package/dist/core/runtime/busy.d.ts +4 -3
  88. package/dist/core/runtime/busy.js +4 -3
  89. package/dist/core/runtime/spawn.js +4 -4
  90. package/dist/core/user-settings.d.ts +17 -2
  91. package/dist/core/user-settings.js +32 -11
  92. package/dist/daemon/api/handlers/human.js +1 -1
  93. package/dist/daemon/api/handlers/inbox.js +22 -7
  94. package/dist/daemon/cron-run.js +16 -2
  95. package/dist/daemon/crtrd.js +1 -1
  96. package/dist/daemon/fleet.d.ts +22 -10
  97. package/dist/daemon/fleet.js +43 -24
  98. package/dist/daemon/human/finish.d.ts +2 -1
  99. package/dist/daemon/human/finish.js +45 -28
  100. package/dist/daemon/human/sweep.js +4 -2
  101. package/dist/daemon/reconcilers/broker-supervision.js +11 -8
  102. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  103. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  104. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  105. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  106. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  107. package/dist/daemon/review/sweep.js +12 -1
  108. package/dist/pages/bundle.css +1 -1
  109. package/dist/pages/bundle.js +1008 -577
  110. package/dist/pages/comments.d.ts +29 -0
  111. package/dist/pages/comments.js +51 -0
  112. package/dist/pages/controls.d.ts +98 -0
  113. package/dist/pages/controls.js +261 -0
  114. package/dist/pages/elements/cards.d.ts +15 -4
  115. package/dist/pages/elements/cards.js +299 -186
  116. package/dist/pages/elements/chart.d.ts +22 -0
  117. package/dist/pages/elements/chart.js +107 -91
  118. package/dist/pages/elements/options.d.ts +9 -0
  119. package/dist/pages/elements/options.js +268 -155
  120. package/dist/pages/elements/pages.d.ts +6 -0
  121. package/dist/pages/elements/pages.js +60 -38
  122. package/dist/pages/elements/slot.d.ts +5 -0
  123. package/dist/pages/elements/slot.js +17 -10
  124. package/dist/pages/elements/table.d.ts +17 -0
  125. package/dist/pages/elements/table.js +336 -228
  126. package/dist/pages/elements/text.d.ts +9 -3
  127. package/dist/pages/elements/text.js +350 -143
  128. package/dist/pages/host.d.ts +0 -24
  129. package/dist/pages/host.js +0 -17
  130. package/dist/pages/readonly.d.ts +24 -0
  131. package/dist/pages/readonly.js +31 -0
  132. package/dist/pages/responses.d.ts +37 -0
  133. package/dist/pages/responses.js +56 -0
  134. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  135. package/dist/pi-extensions/canvas-bash-valve.js +12 -8
  136. package/dist/types.d.ts +19 -5
  137. package/dist/types.js +2 -1
  138. package/package.json +1 -1
  139. package/runtime.lock.json +2 -2
  140. package/scripts/install-runtime.mjs +19 -24
  141. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  142. package/dist/clients/attach/render/html-markdown.js +0 -206
  143. package/dist/core/human/markdown-html.d.ts +0 -8
  144. package/dist/core/human/markdown-html.js +0 -581
@@ -5,7 +5,6 @@ import { defineBranch, defineLeaf } from '../../core/command.js';
5
5
  import { InputError } from '../../core/io.js';
6
6
  import { readConfig } from '../../core/config.js';
7
7
  import { derivePageManifest } from '../../core/human/page.js';
8
- import { checkMarkdown, renderMarkdown } from '../../core/termrender/termrender.js';
9
8
  function documentSource(input, context) {
10
9
  const path = input['path'];
11
10
  const body = input['body'];
@@ -13,8 +12,8 @@ function documentSource(input, context) {
13
12
  if (path === undefined && !stdinProvided) {
14
13
  throw new InputError({
15
14
  error: 'missing_parameter',
16
- message: 'provide exactly one document source: PATH or stdin',
17
- next: 'Run `crtr human doc check -h` or `crtr human doc render -h` and provide a path or pipe a document on stdin.',
15
+ message: 'provide exactly one HTML page source: PATH or stdin',
16
+ next: 'Run `crtr human doc check -h` and provide a complete HTML page path or pipe one on stdin.',
18
17
  });
19
18
  }
20
19
  if (path !== undefined && path !== '-' && stdinProvided && body.trim() !== '') {
@@ -22,7 +21,7 @@ function documentSource(input, context) {
22
21
  error: 'bad_invocation',
23
22
  message: 'both a positional path and piped stdin were provided',
24
23
  field: 'path, body',
25
- next: 'Pass exactly one document source: either PATH or piped stdin, not both.',
24
+ next: 'Pass exactly one page source: either PATH or piped stdin, not both.',
26
25
  });
27
26
  }
28
27
  if (path === undefined || path === '-')
@@ -33,38 +32,22 @@ function documentSource(input, context) {
33
32
  error: 'file_not_found',
34
33
  message: `file not found: ${abs}`,
35
34
  field: 'path',
36
- next: 'Provide a path to an existing markdown or HTML file, or pipe a document on stdin.',
35
+ next: 'Provide a path to an existing complete HTML page or pipe one on stdin.',
37
36
  });
38
37
  }
39
38
  return { body: readFileSync(abs, 'utf8'), path: abs };
40
39
  }
41
- function markdownSource(input, context) {
42
- return documentSource(input, context).body;
43
- }
44
- function pageDialect(source) {
45
- const extension = source.path?.toLowerCase();
46
- if (extension?.endsWith('.html') || extension?.endsWith('.htm'))
47
- return 'html';
48
- if (/^\s*<!doctype\b/i.test(source.body) || /^\s*<html(?:\s|>)/i.test(source.body))
49
- return 'html';
50
- if (source.body.startsWith('---'))
51
- return 'md';
52
- return undefined;
53
- }
54
- function checkPage(source, dialect) {
40
+ function checkPage(source) {
55
41
  const config = readConfig('user');
56
- if (dialect === 'html' && !config.html_pages) {
57
- return { ok: false, error: 'HTML page validation requires the user-scope html_pages capability flag to be enabled (html_pages is false).' };
58
- }
59
42
  let tempDir;
60
43
  try {
61
44
  const sourcePath = source.path ?? (() => {
62
45
  tempDir = mkdtempSync(join(tmpdir(), 'crtr-human-doc-'));
63
- const path = join(tempDir, dialect === 'html' ? 'page.html' : 'page.md');
46
+ const path = join(tempDir, 'page.html');
64
47
  writeFileSync(path, source.body, 'utf8');
65
48
  return path;
66
49
  })();
67
- derivePageManifest(sourcePath, dialect, config.page_components);
50
+ derivePageManifest(sourcePath, 'html', config.page_components);
68
51
  return { ok: true };
69
52
  }
70
53
  catch (error) {
@@ -77,68 +60,32 @@ function checkPage(source, dialect) {
77
60
  }
78
61
  const humanDocCheck = defineLeaf({
79
62
  name: 'check',
80
- description: 'validate a directive-flavored markdown body or page source',
81
- whenToUse: 'validating a page source or review body before submitting it.',
63
+ description: 'validate a complete HTML page source',
64
+ whenToUse: 'validating an authored page before submitting it.',
82
65
  help: {
83
66
  name: 'human doc check',
84
- summary: 'validate directive-flavored markdown or a page through the managed renderer and page schemas',
85
- guide: 'Plain markdown is checked by the pinned renderer. A file with page front matter or an HTML document is parsed as a page; HTML page validation requires the user-scope html_pages capability flag.',
67
+ summary: 'validate a complete HTML page through page schemas',
86
68
  params: [
87
- { kind: 'positional', name: 'path', type: 'path', required: false, constraint: 'Markdown or HTML file to validate. Files ending in .html/.htm, HTML documents beginning with a doctype or <html, and markdown beginning with --- front matter are checked as pages. Provide exactly one of PATH or stdin; use - to read stdin.' },
88
- { kind: 'stdin', name: 'body', required: false, constraint: 'Markdown or HTML to validate. Provide exactly one of piped stdin or PATH.' },
69
+ { kind: 'positional', name: 'path', type: 'path', required: false, constraint: 'Complete HTML page to validate. Provide exactly one of PATH or stdin; use - to read stdin.' },
70
+ { kind: 'stdin', name: 'body', required: false, constraint: 'Complete HTML page to validate. Provide exactly one of piped stdin or PATH.' },
89
71
  ],
90
72
  output: [
91
- { name: 'ok', type: 'boolean', required: true, constraint: 'True when the markdown is valid.' },
92
- { name: 'error', type: 'string', required: false, constraint: 'The renderer or page-parser validation error when ok is false.' },
73
+ { name: 'ok', type: 'boolean', required: true, constraint: 'True when the complete HTML page is valid.' },
74
+ { name: 'error', type: 'string', required: false, constraint: 'The page-parser validation error when ok is false.' },
93
75
  ],
94
76
  outputKind: 'object',
95
- effects: ['Read-only: invokes the private managed renderer or page parser to validate the supplied document.'],
96
- },
97
- run: async (input, context) => {
98
- const source = documentSource(input, context);
99
- const dialect = pageDialect(source);
100
- if (dialect !== undefined)
101
- return checkPage(source, dialect);
102
- return checkMarkdown(source.body);
103
- },
104
- });
105
- const humanDocRender = defineLeaf({
106
- name: 'render',
107
- description: 'render a directive-flavored markdown body',
108
- whenToUse: 'previewing how a page or review body renders before submitting it.',
109
- help: {
110
- name: 'human doc render',
111
- summary: 'render directive-flavored markdown through the pinned managed renderer',
112
- guide: 'The pinned renderer runs from a private managed venv and is not on PATH, so this is the only way to render a page or review body before submitting it.',
113
- params: [
114
- { kind: 'positional', name: 'path', type: 'path', required: false, constraint: 'Markdown file to render. Provide exactly one of PATH or stdin; use - to read stdin.' },
115
- { kind: 'stdin', name: 'body', required: false, constraint: 'Markdown to render. Provide exactly one of piped stdin or PATH.' },
116
- { kind: 'flag', name: 'width', type: 'int', required: false, default: process.stdout.columns || 100, constraint: 'Render width in columns. Default: current stdout width, or 100.' },
117
- { kind: 'flag', name: 'color', type: 'bool', required: false, default: true, constraint: 'Emit ANSI color. Default true; false strips ANSI escape sequences.' },
118
- ],
119
- output: [
120
- { name: 'lines', type: 'string[]', required: true, constraint: 'Rendered terminal lines in display order.' },
121
- { name: 'width', type: 'integer', required: true, constraint: 'The render width in columns.' },
122
- ],
123
- outputKind: 'object',
124
- effects: ['Read-only: invokes the private managed renderer to render the supplied body.'],
125
- },
126
- run: async (input, context) => {
127
- const width = input['width'];
128
- const color = input['color'];
129
- const lines = renderMarkdown(markdownSource(input, context), width);
130
- return { lines: color ? lines : lines.map((line) => line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')), width };
77
+ effects: ['Read-only: invokes the page parser to validate the supplied complete HTML page.'],
131
78
  },
132
- render: (result) => result['lines'].join('\n'),
79
+ run: async (input, context) => checkPage(documentSource(input, context)),
133
80
  });
134
81
  export const humanDoc = defineBranch({
135
82
  name: 'doc',
136
- description: 'validate or render directive-flavored markdown bodies',
137
- whenToUse: 'validating or previewing a directive-flavored markdown body before submitting it.',
83
+ description: 'validate authored HTML pages',
84
+ whenToUse: 'validating an authored HTML page before submitting it.',
138
85
  help: {
139
86
  name: 'human doc',
140
- summary: 'validate and render directive-flavored markdown bodies',
141
- model: 'Both leaves read exactly one body from a path or stdin through the private managed renderer.',
87
+ summary: 'validate authored HTML pages',
88
+ model: 'The check leaf reads exactly one complete HTML page from a path or stdin.',
142
89
  },
143
- children: [humanDocCheck, humanDocRender],
90
+ children: [humanDocCheck],
144
91
  });
@@ -0,0 +1,2 @@
1
+ import { type BranchDef } from '../../core/command.js';
2
+ export declare const humanHtml: BranchDef;
@@ -0,0 +1,81 @@
1
+ import { defineBranch, defineLeaf } from '../../core/command.js';
2
+ import { stateBlock } from '../../core/help.js';
3
+ import { InputError } from '../../core/io.js';
4
+ import { readConfig } from '../../core/config.js';
5
+ import { componentKinds, renderComponentCatalog, renderComponentContract } from '../../core/human/component-docs.js';
6
+ import { pageComponentKinds } from '../../core/human/page-catalog.js';
7
+ function productComponents() {
8
+ return readConfig('user').page_components;
9
+ }
10
+ function productKinds() {
11
+ return pageComponentKinds(productComponents());
12
+ }
13
+ function componentKindState() {
14
+ const kinds = componentKinds(productKinds());
15
+ return stateBlock('kinds', { count: kinds.length }, kinds.join('\n'));
16
+ }
17
+ const humanHtmlList = defineLeaf({
18
+ name: 'list',
19
+ description: 'list page components for selection',
20
+ whenToUse: 'choosing which page component to put in an authored HTML page.',
21
+ help: {
22
+ name: 'human html list',
23
+ summary: 'list page components for selection',
24
+ inputNote: 'No input parameters.',
25
+ output: [
26
+ { name: 'catalog', type: 'markdown', required: true, constraint: 'One concise selection entry per built-in and product-registered component; no component field schemas.' },
27
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Points to `crtr human html show <kind>` for one selected component contract.' },
28
+ ],
29
+ outputKind: 'object',
30
+ effects: ['None. Read-only.'],
31
+ },
32
+ run: async () => ({
33
+ catalog: renderComponentCatalog(productComponents()),
34
+ follow_up: 'Use `crtr human html show <kind>` for one component contract.',
35
+ }),
36
+ render: (result) => `${String(result['catalog'])}\n\n${String(result['follow_up'])}`,
37
+ });
38
+ const humanHtmlShow = defineLeaf({
39
+ name: 'show',
40
+ description: 'show one page component contract',
41
+ whenToUse: 'authoring or validating one selected page component and needing its exact config or response contract.',
42
+ help: {
43
+ name: 'human html show',
44
+ summary: 'show one page component contract',
45
+ params: [
46
+ { kind: 'positional', name: 'kind', type: 'string', required: true, constraint: 'Component kind from the live <kinds> catalog below.' },
47
+ ],
48
+ output: [
49
+ { name: 'kind', type: 'string', required: true, constraint: 'The selected component kind.' },
50
+ { name: 'contract', type: 'markdown', required: true, constraint: 'Hand-written config, response, and display/response-bearing contract for that component.' },
51
+ ],
52
+ outputKind: 'object',
53
+ effects: ['None. Read-only.'],
54
+ dynamicState: componentKindState,
55
+ },
56
+ run: async (input) => {
57
+ const kind = input['kind'];
58
+ const contract = renderComponentContract(kind, productComponents());
59
+ if (contract === undefined) {
60
+ throw new InputError({
61
+ error: 'unknown_component',
62
+ message: `unknown page component kind "${kind}"`,
63
+ field: 'kind',
64
+ next: 'Run `crtr human html show -h` and choose a kind from its live catalog.',
65
+ });
66
+ }
67
+ return { kind, contract };
68
+ },
69
+ render: (result) => String(result['contract']),
70
+ });
71
+ export const humanHtml = defineBranch({
72
+ name: 'html',
73
+ description: 'discover HTML page components and contracts',
74
+ whenToUse: 'authoring an HTML page and choosing a component or needing one component’s exact contract.',
75
+ help: {
76
+ name: 'human html',
77
+ summary: 'discover HTML page components and contracts',
78
+ model: 'List is the selection catalog; show owns the full config and response contract for one selected component.',
79
+ },
80
+ children: [humanHtmlList, humanHtmlShow],
81
+ });
@@ -2,10 +2,11 @@ import type { PageSource } from '../../core/human/page-schema.js';
2
2
  /** Provenance for crouter tickets. Ticket *identity* comes from the bridge node
3
3
  * directory, never ticket data; `nodeId` carries the originating node so a
4
4
  * consumer can relate a ticket back to whatever that node belongs to, and
5
- * `sessionName` is a presentation label only. Review tickets already stamp
6
- * `nodeId` the same way (`core/review/realize.ts`), so pages match them.
7
- * `nodeId` is stamped even when the daemon is unreachable — losing the label is
8
- * survivable, losing machine attribution is not. */
5
+ * `sessionName` is a presentation label only, while `profileName` lets product
6
+ * consumers resolve the applet or worker identity behind the ticket. Review
7
+ * tickets stamp the same provenance (`core/review/realize.ts`), so pages match
8
+ * them. `nodeId` is stamped even when the daemon is unreachable — losing the
9
+ * labels is survivable, losing machine attribution is not. */
9
10
  export declare function sourceStamp(): Promise<PageSource>;
10
11
  export declare const humanAsk: import("../../core/command.js").LeafDef;
11
12
  export declare const humanNotify: import("../../core/command.js").LeafDef;
@@ -1,20 +1,15 @@
1
- /**
2
- * Prompt-design gap: a person opens asks from background agents without the
3
- * task or conversation context. Human-ask help therefore teaches agents to
4
- * make every interruption self-contained and answerable before its evidence.
5
- */
6
1
  import { defineLeaf } from '../../core/command.js';
7
2
  import { InputError } from '../../core/io.js';
8
3
  import { cliClient } from '../api-client.js';
9
4
  import { extname, join } from 'node:path';
10
5
  import { randomBytes } from 'node:crypto';
11
- import { rmSync } from 'node:fs';
6
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
12
8
  import { readConfig } from '../../core/config.js';
13
9
  import { markStopSignal } from '../../core/runtime/stop-signals.js';
14
10
  import { atomicWriteJson } from '../../core/human/convention.js';
15
11
  import { ticketDir } from '../../core/human/root.js';
16
12
  import { preparePage, publishPage } from '../../core/human/tickets.js';
17
- import { synthesizedPageDocument, writeSynthesizedPage } from '../../core/human/page-synth.js';
18
13
  import { PageAuthoringError } from '../../core/human/page.js';
19
14
  import { display } from '../../core/termrender/display.js';
20
15
  import { pageHelpText, resolveMaxPanes } from './shared.js';
@@ -26,76 +21,45 @@ function askingNode() {
26
21
  /** Provenance for crouter tickets. Ticket *identity* comes from the bridge node
27
22
  * directory, never ticket data; `nodeId` carries the originating node so a
28
23
  * consumer can relate a ticket back to whatever that node belongs to, and
29
- * `sessionName` is a presentation label only. Review tickets already stamp
30
- * `nodeId` the same way (`core/review/realize.ts`), so pages match them.
31
- * `nodeId` is stamped even when the daemon is unreachable — losing the label is
32
- * survivable, losing machine attribution is not. */
24
+ * `sessionName` is a presentation label only, while `profileName` lets product
25
+ * consumers resolve the applet or worker identity behind the ticket. Review
26
+ * tickets stamp the same provenance (`core/review/realize.ts`), so pages match
27
+ * them. `nodeId` is stamped even when the daemon is unreachable — losing the
28
+ * labels is survivable, losing machine attribution is not. */
33
29
  export async function sourceStamp() {
34
30
  const nodeId = askingNode();
35
31
  if (nodeId === null || nodeId === '')
36
32
  return {};
37
33
  try {
38
- const sessionName = (await cliClient().getNode(nodeId)).name.trim();
39
- return sessionName === '' ? { nodeId } : { nodeId, sessionName };
34
+ const client = cliClient();
35
+ const node = await client.getNode(nodeId);
36
+ const sessionName = node.name.trim();
37
+ const profileName = node.profile_id === null
38
+ ? undefined
39
+ : (await client.getProfile(node.profile_id)).name;
40
+ return {
41
+ nodeId,
42
+ ...(sessionName === '' ? {} : { sessionName }),
43
+ ...(profileName === undefined ? {} : { profileName }),
44
+ };
40
45
  }
41
46
  catch {
42
47
  return { nodeId };
43
48
  }
44
49
  }
45
- function requireAskTitle(title) {
46
- if (title !== undefined)
47
- return title;
48
- throw new InputError({
49
- error: 'missing_parameter',
50
- message: 'stdin questions require a non-empty --title',
51
- field: 'title',
52
- next: 'Retry with a short --title naming the topic being decided.',
53
- });
54
- }
55
- function requireSubtitle(subtitle, field = 'subtitle') {
56
- if (subtitle !== undefined && subtitle.trim() !== '')
57
- return subtitle.trim();
58
- throw new InputError({
59
- error: 'missing_parameter',
60
- message: 'a non-empty subtitle is required',
61
- field,
62
- next: 'Write one plain-English sentence stating the decision, recommendation or status, and stakes.',
63
- });
64
- }
65
50
  function pageDialect(sourceFile) {
66
51
  const extension = extname(sourceFile).toLowerCase();
67
- if (extension === '.md')
68
- return 'md';
69
52
  if (extension === '.html' || extension === '.htm')
70
53
  return 'html';
71
54
  throw new InputError({
72
55
  error: 'invalid_page_dialect',
73
- message: `--page must point to a .md, .html, or .htm file; received "${sourceFile}"`,
56
+ message: `--page must name an HTML page; received "${sourceFile}"`,
74
57
  field: 'page',
75
- next: 'Use a Markdown page (.md) or an HTML page (.html or .htm).',
58
+ next: 'Write the page at `$CRTR_CONTEXT_DIR/pages/<name>.html`, then retry with --page PATH.',
76
59
  });
77
60
  }
78
- function pageProductKinds(dialect) {
79
- const config = readConfig('user');
80
- if (dialect === 'html' && !config.html_pages) {
81
- throw new InputError({
82
- error: 'page_dialect_disabled',
83
- message: 'html pages are not enabled in this home; md is the dialect to use',
84
- field: 'page',
85
- next: 'Use a .md page, or enable html_pages in the user scope config before submitting HTML.',
86
- });
87
- }
88
- return config.page_components;
89
- }
90
- function requireNotifyTitle(title) {
91
- if (title !== undefined && title.trim() !== '')
92
- return title.trim();
93
- throw new InputError({
94
- error: 'missing_parameter',
95
- message: 'notify shorthand requires a non-empty title',
96
- field: 'title',
97
- next: 'Retry with a positional title, --subtitle, and optional --body, or submit a page with --page PATH.',
98
- });
61
+ function pageProductKinds() {
62
+ return readConfig('user').page_components;
99
63
  }
100
64
  function submitAuthoredPage(submit) {
101
65
  try {
@@ -112,10 +76,50 @@ function submitAuthoredPage(submit) {
112
76
  });
113
77
  }
114
78
  }
79
+ function writePipedPage(document) {
80
+ const dir = mkdtempSync(join(tmpdir(), 'crtr-human-page-'));
81
+ const path = join(dir, 'page.html');
82
+ writeFileSync(path, document, { encoding: 'utf8', mode: 0o600 });
83
+ return { dir, path };
84
+ }
85
+ /** Resolve exactly one authored page input. Piped bytes are materialized so
86
+ * both input forms follow the same page preparation and validation path. */
87
+ function pageInput(input) {
88
+ const file = input['page'];
89
+ const documentRaw = input['document'];
90
+ // Optional stdin arrives as an empty string when a caller redirects an empty
91
+ // stream. It is not a page source and must not conflict with --page.
92
+ const document = typeof documentRaw === 'string' && documentRaw.trim() !== '' ? documentRaw : undefined;
93
+ if (file !== undefined && document !== undefined) {
94
+ throw new InputError({
95
+ error: 'bad_invocation',
96
+ message: 'received both --page and a piped page document',
97
+ field: 'page, document',
98
+ next: 'Pass exactly one page source: --page PATH or the document on stdin.',
99
+ });
100
+ }
101
+ if (file === undefined && document === undefined) {
102
+ throw new InputError({
103
+ error: 'missing_parameter',
104
+ message: 'a page source is required',
105
+ field: 'page, document',
106
+ next: 'Pass exactly one page source: --page PATH or the document on stdin.',
107
+ });
108
+ }
109
+ if (file !== undefined) {
110
+ if (typeof file !== 'string')
111
+ throw new InputError({ error: 'invalid_type', message: '--page must be a path', field: 'page', next: 'Retry with --page PATH.' });
112
+ return { sourceFile: file };
113
+ }
114
+ if (typeof document !== 'string')
115
+ throw new InputError({ error: 'invalid_type', message: 'stdin must contain a page document', field: 'document', next: 'Pipe the page document on stdin.' });
116
+ const temp = writePipedPage(document);
117
+ return { sourceFile: temp.path, tempDir: temp.dir };
118
+ }
115
119
  /** The non-blocking status peek returned by ask/review. How the human reaches
116
- * their inbox is theirs, not the agent's — naming a shortcut here only teaches
117
- * agents to repeat a screen-specific gesture back to a reader who may be on a
118
- * different surface entirely. */
120
+ * their inbox is theirs, not the agent's — naming a shortcut here only teaches
121
+ * agents to repeat a screen-specific gesture back to a reader who may be on a
122
+ * different surface entirely. */
119
123
  function queuedFollowUp() {
120
124
  return 'Queued for the human. Their answer is pushed to your inbox and wakes you when they respond — never poll, verify it opened, or block.';
121
125
  }
@@ -128,20 +132,15 @@ export const humanAsk = defineLeaf({
128
132
  whenToUse: 'you would otherwise lay a decision, a set of options, or a question out for the user as prose — reach for this instead, for anything from a quick yes/no to a judgment-heavy call: gating a handoff, merge, deploy, destructive/irreversible operation, or real budget on an explicit human sign-off (offer Yes/No options), reviewing all the requirements before building, choosing among implementation patterns, walking a list of risks and deciding what to do about each, settling a naming or scope question, picking which of several findings to act on. Works for open-ended asks too (set `allowFreetext`, offer a few `options` as starting points).',
129
133
  help: {
130
134
  name: 'human ask',
131
- summary: "submit a decision ticket to crouter's inbox; returns a job handle immediately.",
132
- guide: [
133
- '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`.',
134
- PAGE_HELP.authoring,
135
- ].join('\n\n'),
135
+ summary: "submit a decision ticket to crouter's inbox; returns a job handle immediately",
136
+ guide: 'Page authoring: `crtr human -h`.',
136
137
  params: [
137
- { kind: 'flag', name: 'title', type: 'string', required: false, constraint: 'Required with stdin shorthand: a short ≤~4-word inbox topic. Mutually exclusive with --page.' },
138
- { 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.' },
139
- { 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." },
140
- { kind: 'flag', name: 'page', type: 'path', required: false, constraint: `${PAGE_HELP.pageHint} Mutually exclusive with shorthand.` },
138
+ { kind: 'stdin', name: 'document', required: false, allowPositional: false, constraint: 'Page bytes from stdin. Provide exactly one of stdin or --page PATH.' },
139
+ { kind: 'flag', name: 'page', type: 'path', required: false, constraint: `${PAGE_HELP.pageHint} Provide exactly one of --page or stdin.` },
141
140
  ],
142
141
  output: [
143
142
  { 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.' },
144
- { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding page.md or page.html, page.json, run.json, and response.json.' },
143
+ { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding page.html, page.json, run.json, and response.json.' },
145
144
  { name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking status peek; the answer arrives via your inbox.' },
146
145
  ],
147
146
  outputKind: 'object',
@@ -151,48 +150,10 @@ export const humanAsk = defineLeaf({
151
150
  ],
152
151
  },
153
152
  run: async (input) => {
154
- // Optional stdin reaches the leaf as an empty string when omitted. Keep the
155
- // raw non-empty bytes untouched so shorthand callers retain their body.
156
- const titleRaw = input['title'];
157
- const title = typeof titleRaw === 'string' && titleRaw.trim() !== '' ? titleRaw.trim() : undefined;
158
- const questionRaw = input['question'];
159
- const question = typeof questionRaw === 'string' && questionRaw !== '' ? questionRaw : undefined;
160
- const subtitleRaw = input['subtitle'];
161
- const subtitle = typeof subtitleRaw === 'string' && subtitleRaw.trim() !== '' ? subtitleRaw.trim() : undefined;
162
- const pageInput = input['page'];
163
- if (pageInput !== undefined && (question !== undefined || titleRaw !== undefined || subtitleRaw !== undefined)) {
164
- throw new InputError({
165
- error: 'bad_invocation',
166
- message: 'use --title and --subtitle with stdin question text, or --page, not both',
167
- next: 'Retry with --title, --subtitle, and a piped question body, or with --page PATH alone.',
168
- });
169
- }
170
- if (question === undefined && pageInput === undefined) {
171
- throw new InputError({
172
- error: 'missing_parameter',
173
- message: 'human ask needs stdin question text or --page PATH',
174
- next: 'Pipe a question body with --title and --subtitle, or pass --page PATH for an authored page.',
175
- });
176
- }
177
- const dialect = pageDialect(typeof pageInput === 'string' ? pageInput : 'page.md');
178
- const productKinds = pageProductKinds(dialect);
179
- let tempDir;
180
- let sourceFile;
181
- if (pageInput !== undefined) {
182
- sourceFile = pageInput;
183
- }
184
- else {
185
- const simpleAsk = { title: requireAskTitle(title), subtitle: requireSubtitle(subtitle), question: question };
186
- const synthesized = writeSynthesizedPage(synthesizedPageDocument(simpleAsk.title, simpleAsk.subtitle, simpleAsk.question, true));
187
- tempDir = synthesized.dir;
188
- sourceFile = synthesized.path;
189
- }
153
+ const { sourceFile, tempDir } = pageInput(input);
190
154
  try {
191
155
  const source = await sourceStamp();
192
- // The page is validated before the bridge node exists: a rejected page
193
- // must create nothing, because a bridge node born for a ticket that never
194
- // got published is a live terminal node no answer can ever finish.
195
- const prepared = submitAuthoredPage(() => preparePage({ sourceFile, dialect, source, productKinds }));
156
+ const prepared = submitAuthoredPage(() => preparePage({ sourceFile, dialect: pageDialect(sourceFile), source, productKinds: pageProductKinds() }));
196
157
  const cwd = process.cwd();
197
158
  const jobId = (await cliClient().createHumanBridge({ kind: 'human', parent: askingNode(), cwd, name: 'human-ask' })).node_id;
198
159
  const idir = ticketDir(jobId);
@@ -220,19 +181,14 @@ export const humanNotify = defineLeaf({
220
181
  help: {
221
182
  name: 'human notify',
222
183
  summary: 'enqueue a fire-and-forget acknowledgement in the human inbox; creates no job',
223
- guide: [
224
- 'State what happened in plain words. Shorthand bodies are directive-flavored Markdown; validate authored pages with `crtr human doc check`.',
225
- PAGE_HELP.authoring,
226
- ].join('\n\n'),
184
+ guide: 'Page authoring: `crtr human -h`.',
227
185
  params: [
228
- { kind: 'positional', name: 'title', type: 'string', required: false, constraint: 'Notification headline for shorthand; required with --subtitle when --page is omitted.' },
229
- { kind: 'flag', name: 'subtitle', type: 'string', required: false, constraint: 'One plain-English sentence stating the notification status and stakes; required when --page is omitted.' },
230
- { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Optional markdown body for shorthand; mutually exclusive with --page.' },
231
- { kind: 'flag', name: 'page', type: 'path', required: false, constraint: `${PAGE_HELP.pageHint} Mutually exclusive with --body.` },
186
+ { kind: 'stdin', name: 'document', required: false, allowPositional: false, constraint: 'Page bytes from stdin. Provide exactly one of stdin or --page PATH.' },
187
+ { kind: 'flag', name: 'page', type: 'path', required: false, constraint: `${PAGE_HELP.pageHint} Provide exactly one of --page or stdin.` },
232
188
  ],
233
189
  output: [
234
190
  { name: 'queued', type: 'boolean', required: true, constraint: 'Always true — the notify page is durable inbox state the human acknowledges from the inbox.' },
235
- { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding page.md or page.html, page.json, and run.json.' },
191
+ { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding page.html, page.json, and run.json.' },
236
192
  ],
237
193
  outputKind: 'object',
238
194
  effects: [
@@ -240,38 +196,10 @@ export const humanNotify = defineLeaf({
240
196
  ],
241
197
  },
242
198
  run: async (input) => {
243
- const titleRaw = input['title'];
244
- const title = typeof titleRaw === 'string' && titleRaw.trim() !== '' ? titleRaw.trim() : undefined;
245
- const subtitleRaw = input['subtitle'];
246
- const subtitle = typeof subtitleRaw === 'string' && subtitleRaw.trim() !== '' ? subtitleRaw.trim() : undefined;
247
- const body = input['body'];
248
- const pageInput = input['page'];
249
- if (pageInput !== undefined && body !== undefined) {
250
- throw new InputError({
251
- error: 'bad_invocation',
252
- message: 'use --page or --body, not both',
253
- next: 'Retry with --page PATH alone for an authored page, or use the title, --subtitle, and --body shorthand.',
254
- });
255
- }
256
- const dialect = pageDialect(typeof pageInput === 'string' ? pageInput : 'page.md');
257
- const productKinds = pageProductKinds(dialect);
258
- let tempDir;
259
- let sourceFile;
260
- if (pageInput !== undefined) {
261
- sourceFile = pageInput;
262
- }
263
- else {
264
- const shorthandTitle = requireNotifyTitle(title);
265
- const shorthandSubtitle = requireSubtitle(subtitle);
266
- const synthesized = writeSynthesizedPage(synthesizedPageDocument(shorthandTitle, shorthandSubtitle, body ?? '', false));
267
- tempDir = synthesized.dir;
268
- sourceFile = synthesized.path;
269
- }
199
+ const { sourceFile, tempDir } = pageInput(input);
270
200
  try {
271
201
  const source = await sourceStamp();
272
- // Validate before the ticket directory exists, so a rejected page leaves
273
- // no half-built ticket behind.
274
- const prepared = submitAuthoredPage(() => preparePage({ sourceFile, dialect, source, productKinds }));
202
+ const prepared = submitAuthoredPage(() => preparePage({ sourceFile, dialect: pageDialect(sourceFile), source, productKinds: pageProductKinds() }));
275
203
  const id = `nfy-${randomBytes(4).toString('hex')}`;
276
204
  const rc = { mode: 'notify' };
277
205
  atomicWriteJson(join(ticketDir(id), 'run.json'), rc);
@@ -286,8 +214,8 @@ export const humanNotify = defineLeaf({
286
214
  });
287
215
  // ---------------------------------------------------------------------------
288
216
  // show (no job, non-blocking passthrough) — passive live document display, NOT
289
- // a ticket surface. Retained per the tmux-inbox cutover: `display()` puts a
290
- // file live in a watch pane and never enqueues anything.
217
+ // a ticket surface. `display()` puts a file live in a watch pane and never
218
+ // enqueues anything.
291
219
  // ---------------------------------------------------------------------------
292
220
  export const humanShow = defineLeaf({
293
221
  name: 'show',
@@ -296,7 +224,7 @@ export const humanShow = defineLeaf({
296
224
  help: {
297
225
  name: 'human show',
298
226
  summary: "put a file live on screen in a tmux pane with crouter's renderer",
299
- 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`.',
227
+ 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). To put a rich document in front of the human as a durable inbox page, use `crtr human notify --page PATH`.',
300
228
  params: [
301
229
  { kind: 'positional', name: 'path', type: 'path', required: true, constraint: 'Path to the file to render.' },
302
230
  { kind: 'flag', name: 'window', type: 'enum', choices: ['auto', 'split', 'new'], required: false, default: 'auto', constraint: 'Placement. Default auto.' },
@@ -1,17 +1,16 @@
1
1
  export interface PageHelpText {
2
2
  authoring: string;
3
+ componentSelection: readonly string[];
3
4
  pageHint: string;
5
+ rootUseWhen: string;
4
6
  }
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;
7
+ /** The one HTML authoring model for `human -h`. */
8
+ export declare function pageHelpText(htmlSurface?: boolean): PageHelpText;
9
9
  /**
10
10
  * Crouter's private per-interaction record. Reduced to the delivery essentials:
11
11
  * the mode that identifies the ticket and the bridge node id it delivers back to.
12
- * The ticket store owns every public request/result/output path (page.md/page.html,
12
+ * The ticket store owns every public request/result/output path (page.html,
13
13
  * page.json, response.json, review.json); crouter never records pane ids or output paths.
14
- *
15
14
  */
16
15
  export interface RunRecord {
17
16
  mode: 'ask' | 'notify' | 'review';