@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.
- package/dist/api/client.d.ts +2 -2
- package/dist/api/dto/inbox.d.ts +46 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -3
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/render/chat-view.d.ts +3 -8
- package/dist/clients/attach/render/chat-view.js +3 -24
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/viewer.js +613 -602
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
- package/dist/clients/inbox/controller.js +4 -4
- package/dist/clients/inbox/page-adapter.d.ts +2 -1
- package/dist/clients/inbox/tui/render.js +46 -25
- package/dist/clients/inbox/tui/slots.d.ts +1 -3
- package/dist/clients/inbox/tui/slots.js +8 -37
- package/dist/clients/inbox/tui/types.d.ts +4 -3
- package/dist/commands/__tests__/human.test.js +160 -23
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/human/doc.js +21 -74
- package/dist/commands/human/html.d.ts +2 -0
- package/dist/commands/human/html.js +81 -0
- package/dist/commands/human/prompts.d.ts +5 -4
- package/dist/commands/human/prompts.js +81 -153
- package/dist/commands/human/shared.d.ts +5 -6
- package/dist/commands/human/shared.js +13 -61
- package/dist/commands/human.js +7 -2
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/human-deliver.test.js +28 -37
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
- package/dist/core/canvas/__tests__/attention.test.js +2 -2
- package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +26 -0
- package/dist/core/canvas/canvas.js +55 -0
- package/dist/core/command.js +6 -11
- package/dist/core/config.js +3 -2
- package/dist/core/help.d.ts +3 -0
- package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/html-markdown.test.js +52 -0
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +16 -0
- package/dist/core/human/__tests__/page-render.test.js +40 -23
- package/dist/core/human/__tests__/page-tickets.test.js +48 -27
- package/dist/core/human/__tests__/page.test.js +48 -65
- package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +51 -0
- package/dist/core/human/answer.js +125 -0
- package/dist/core/human/component-docs.d.ts +14 -0
- package/dist/core/human/component-docs.js +161 -0
- package/dist/core/human/html-markdown.d.ts +3 -0
- package/dist/core/human/html-markdown.js +288 -0
- package/dist/core/human/page-catalog.d.ts +19 -3
- package/dist/core/human/page-catalog.js +73 -16
- package/dist/core/human/page-render.d.ts +3 -2
- package/dist/core/human/page-render.js +19 -45
- package/dist/core/human/page-schema.d.ts +108 -8
- package/dist/core/human/page-schema.js +88 -31
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +23 -7
- package/dist/core/human/page.d.ts +14 -7
- package/dist/core/human/page.js +178 -90
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/scan.d.ts +3 -2
- package/dist/core/human/scan.js +7 -8
- package/dist/core/human/summary.d.ts +4 -7
- package/dist/core/human/summary.js +4 -60
- package/dist/core/human/tickets.d.ts +10 -9
- package/dist/core/human/tickets.js +5 -3
- package/dist/core/human/types.d.ts +4 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/review/realize.js +9 -1
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker.js +8 -6
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/user-settings.d.ts +17 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/inbox.js +22 -7
- package/dist/daemon/cron-run.js +16 -2
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +2 -1
- package/dist/daemon/human/finish.js +45 -28
- package/dist/daemon/human/sweep.js +4 -2
- package/dist/daemon/reconcilers/broker-supervision.js +11 -8
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pages/bundle.css +1 -1
- package/dist/pages/bundle.js +1008 -577
- package/dist/pages/comments.d.ts +29 -0
- package/dist/pages/comments.js +51 -0
- package/dist/pages/controls.d.ts +98 -0
- package/dist/pages/controls.js +261 -0
- package/dist/pages/elements/cards.d.ts +15 -4
- package/dist/pages/elements/cards.js +299 -186
- package/dist/pages/elements/chart.d.ts +22 -0
- package/dist/pages/elements/chart.js +107 -91
- package/dist/pages/elements/options.d.ts +9 -0
- package/dist/pages/elements/options.js +268 -155
- package/dist/pages/elements/pages.d.ts +6 -0
- package/dist/pages/elements/pages.js +60 -38
- package/dist/pages/elements/slot.d.ts +5 -0
- package/dist/pages/elements/slot.js +17 -10
- package/dist/pages/elements/table.d.ts +17 -0
- package/dist/pages/elements/table.js +336 -228
- package/dist/pages/elements/text.d.ts +9 -3
- package/dist/pages/elements/text.js +350 -143
- package/dist/pages/host.d.ts +0 -24
- package/dist/pages/host.js +0 -17
- package/dist/pages/readonly.d.ts +24 -0
- package/dist/pages/readonly.js +31 -0
- package/dist/pages/responses.d.ts +37 -0
- package/dist/pages/responses.js +56 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +12 -8
- package/dist/types.d.ts +19 -5
- package/dist/types.js +2 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/install-runtime.mjs +19 -24
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/core/human/markdown-html.d.ts +0 -8
- 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
|
|
17
|
-
next: 'Run `crtr human doc check -h`
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
46
|
+
const path = join(tempDir, 'page.html');
|
|
64
47
|
writeFileSync(path, source.body, 'utf8');
|
|
65
48
|
return path;
|
|
66
49
|
})();
|
|
67
|
-
derivePageManifest(sourcePath,
|
|
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
|
|
81
|
-
whenToUse: 'validating
|
|
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
|
|
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: '
|
|
88
|
-
{ kind: 'stdin', name: 'body', required: false, constraint: '
|
|
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
|
|
92
|
-
{ name: 'error', type: 'string', required: false, constraint: 'The
|
|
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
|
|
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
|
-
|
|
79
|
+
run: async (input, context) => checkPage(documentSource(input, context)),
|
|
133
80
|
});
|
|
134
81
|
export const humanDoc = defineBranch({
|
|
135
82
|
name: 'doc',
|
|
136
|
-
description: 'validate
|
|
137
|
-
whenToUse: 'validating
|
|
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
|
|
141
|
-
model: '
|
|
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
|
|
90
|
+
children: [humanDocCheck],
|
|
144
91
|
});
|
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
39
|
-
|
|
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
|
|
56
|
+
message: `--page must name an HTML page; received "${sourceFile}"`,
|
|
74
57
|
field: 'page',
|
|
75
|
-
next: '
|
|
58
|
+
next: 'Write the page at `$CRTR_CONTEXT_DIR/pages/<name>.html`, then retry with --page PATH.',
|
|
76
59
|
});
|
|
77
60
|
}
|
|
78
|
-
function pageProductKinds(
|
|
79
|
-
|
|
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
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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: '
|
|
138
|
-
{ kind: 'flag', name: '
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
229
|
-
{ kind: 'flag', name: '
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
290
|
-
//
|
|
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)
|
|
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
|
-
/**
|
|
6
|
-
|
|
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.
|
|
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';
|