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