@north-light/crouter 0.3.197 → 0.3.199
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 +19 -19
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +126 -33
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +8 -8
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +29 -9
- package/dist/clients/attach/render/chat-view.js +64 -35
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -982
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +3 -4
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +101 -36
- package/dist/clients/inbox/tui/slots.d.ts +4 -4
- package/dist/clients/inbox/tui/slots.js +19 -44
- package/dist/clients/inbox/tui/types.d.ts +6 -10
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +118 -24
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +17 -8
- package/dist/commands/human/prompts.js +217 -226
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +4 -15
- package/dist/commands/human/shared.js +19 -61
- package/dist/commands/human.js +32 -19
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- 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__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +41 -45
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- 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 +33 -0
- package/dist/core/canvas/canvas.js +92 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +8 -12
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +138 -4
- package/dist/core/help.d.ts +3 -0
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +20 -0
- package/dist/core/human/__tests__/page-tickets.test.js +68 -34
- package/dist/core/human/__tests__/page.test.js +60 -119
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
- 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 +53 -0
- package/dist/core/human/answer.js +119 -0
- package/dist/core/human/component-docs.d.ts +18 -0
- package/dist/core/human/component-docs.js +434 -0
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +56 -5
- package/dist/core/human/page-catalog.js +151 -22
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +48 -116
- package/dist/core/human/page-schema.js +100 -72
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +20 -7
- package/dist/core/human/page.d.ts +13 -21
- package/dist/core/human/page.js +147 -391
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -3
- package/dist/core/human/scan.js +21 -21
- package/dist/core/human/summary.d.ts +2 -6
- package/dist/core/human/summary.js +3 -59
- package/dist/core/human/tickets.d.ts +22 -15
- package/dist/core/human/tickets.js +90 -28
- package/dist/core/human/types.d.ts +14 -2
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +12 -4
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +25 -12
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +36 -2
- package/dist/core/user-settings.js +56 -14
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +48 -109
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +29 -17
- 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 +12 -6
- package/dist/daemon/human/finish.js +134 -54
- package/dist/daemon/human/sweep.js +9 -3
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +14 -9
- 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/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +31 -13
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +61 -5
- package/dist/types.js +8 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/scripts/install-runtime.mjs +19 -24
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/commands/human/doc.js +0 -144
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/page-render.test.js +0 -49
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
- package/dist/core/human/page-render.d.ts +0 -3
- package/dist/core/human/page-render.js +0 -131
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1347
- package/dist/pages/comments.d.ts +0 -43
- package/dist/pages/comments.js +0 -125
- package/dist/pages/elements/cards.d.ts +0 -17
- package/dist/pages/elements/cards.js +0 -802
- package/dist/pages/elements/chart.js +0 -648
- package/dist/pages/elements/options.d.ts +0 -21
- package/dist/pages/elements/options.js +0 -629
- package/dist/pages/elements/pages.d.ts +0 -24
- package/dist/pages/elements/pages.js +0 -355
- package/dist/pages/elements/slot.d.ts +0 -21
- package/dist/pages/elements/slot.js +0 -105
- package/dist/pages/elements/table.d.ts +0 -19
- package/dist/pages/elements/table.js +0 -935
- package/dist/pages/elements/text.d.ts +0 -25
- package/dist/pages/elements/text.js +0 -968
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -158
- package/dist/pages/host.js +0 -182
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -6,15 +6,15 @@ 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
|
-
import {
|
|
11
|
+
import { humanSend } from '../../commands/human/prompts.js';
|
|
13
12
|
import { getNode, listNodes, closeDb } from '../canvas/index.js';
|
|
14
13
|
import { readInboxSince } from '../feed/inbox.js';
|
|
15
14
|
import { resolveRef } from '../canvas/history.js';
|
|
16
15
|
import { apiSocketPath } from '../canvas/paths.js';
|
|
17
16
|
import { spawnNode } from '../runtime/nodes.js';
|
|
17
|
+
import { bindTicketReplyRoute } from '../human/convention.js';
|
|
18
18
|
import { ticketDir } from '../human/root.js';
|
|
19
19
|
import { readTicketResult, submitPage } from '../human/tickets.js';
|
|
20
20
|
let home;
|
|
@@ -28,18 +28,6 @@ async function waitForSocket(path) {
|
|
|
28
28
|
}
|
|
29
29
|
throw new Error(`api socket never bound at ${path}`);
|
|
30
30
|
}
|
|
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
31
|
async function withMutedStdout(fn) {
|
|
44
32
|
const originalWrite = process.stdout.write;
|
|
45
33
|
process.stdout.write = (() => true);
|
|
@@ -52,26 +40,17 @@ async function withMutedStdout(fn) {
|
|
|
52
40
|
}
|
|
53
41
|
function bridgeUnder() {
|
|
54
42
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
55
|
-
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-
|
|
43
|
+
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
|
|
56
44
|
return { parent, bridge };
|
|
57
45
|
}
|
|
58
46
|
function submitBridgePage(bridge) {
|
|
59
47
|
const dir = ticketDir(bridge);
|
|
60
48
|
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'));
|
|
49
|
+
const sourceFile = join(sourceDir, 'page.tsx');
|
|
50
|
+
writeFileSync(sourceFile, 'export default () => (\n <Page title="Ship it?" subtitle="Decide whether to ship now so the release can proceed.">\n <UserText id="question" initialText="" />\n </Page>\n);\n');
|
|
73
51
|
try {
|
|
74
|
-
|
|
52
|
+
bindTicketReplyRoute(dir, bridge);
|
|
53
|
+
submitPage({ dir, sourceFile, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
75
54
|
}
|
|
76
55
|
finally {
|
|
77
56
|
rmSync(sourceDir, { recursive: true, force: true });
|
|
@@ -110,41 +89,54 @@ after(() => {
|
|
|
110
89
|
delete process.env['CRTR_NODE_ID'];
|
|
111
90
|
});
|
|
112
91
|
describe('enqueue is producer-only', () => {
|
|
113
|
-
test('
|
|
92
|
+
test('send preserves title and subtitle from the authored page', async () => {
|
|
114
93
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
115
94
|
process.env['CRTR_NODE_ID'] = parent;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
assert.
|
|
95
|
+
const page = join(home, 'release-decision.tsx');
|
|
96
|
+
writeFileSync(page, 'export default () => (\n <Page title="Release decision" subtitle="Decide whether to release now so the launch can proceed.">\n <p>This first line stays page content.</p>\n <UserText id="question" initialText="" />\n </Page>\n);\n');
|
|
97
|
+
await assert.rejects(() => humanSend.run({}), /a page source is required/);
|
|
98
|
+
await assert.rejects(() => humanSend.run({ page, document: 'export default () => <Page title="Dup" />;' }), /received both --page and a piped page document/);
|
|
99
|
+
const result = (await humanSend.run({ page, document: '' }));
|
|
100
|
+
assert.equal(typeof result['page_id'], 'string');
|
|
101
|
+
assert.equal(result['reply'], true);
|
|
119
102
|
assert.equal(typeof result['dir'], 'string');
|
|
120
103
|
for (const forbidden of ['pane_id', 'render_pane_id', 'pid', 'worker_pid'])
|
|
121
|
-
assert.ok(!(forbidden in result), `
|
|
104
|
+
assert.ok(!(forbidden in result), `send result must not expose ${forbidden}`);
|
|
122
105
|
const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
|
|
123
106
|
assert.equal(manifest.title, 'Release decision');
|
|
124
107
|
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
|
|
108
|
+
const document = readFileSync(join(result['dir'], 'page.tsx'), 'utf8');
|
|
109
|
+
assert.ok(document.includes('This first line stays page content.'));
|
|
110
|
+
assert.equal(manifest.slots.filter((slot) => slot.kind === 'text').length, 1);
|
|
111
|
+
});
|
|
112
|
+
test('send materializes a piped page before validation and publication', async () => {
|
|
113
|
+
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
114
|
+
process.env['CRTR_NODE_ID'] = parent;
|
|
115
|
+
const document = 'export default () => (\n <Page title="Piped question" subtitle="Answer the question to unblock this task.">\n <UserText id="question" initialText="" />\n </Page>\n);\n';
|
|
116
|
+
const result = (await humanSend.run({ document }));
|
|
117
|
+
const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
|
|
118
|
+
assert.equal(manifest.title, 'Piped question');
|
|
119
|
+
assert.equal(readFileSync(join(result['dir'], 'page.tsx'), 'utf8'), document);
|
|
128
120
|
});
|
|
129
121
|
});
|
|
130
122
|
describe('terminal ticket delivery', () => {
|
|
131
123
|
test('resolvePageTicket pushFinals the readable answer once and wakes the subscribed parent', async () => {
|
|
132
124
|
const { parent, bridge } = bridgeUnder();
|
|
133
125
|
const dir = submitBridgePage(bridge);
|
|
134
|
-
const result = await resolvePageTicket(bridge, { question: { text: 'yes, ship', edited: true
|
|
126
|
+
const result = await resolvePageTicket(bridge, { question: { text: 'yes, ship', edited: true } });
|
|
135
127
|
assert.equal(result.kind, 'page');
|
|
136
128
|
closeDb();
|
|
137
129
|
assert.equal(getNode(bridge)?.status, 'done');
|
|
138
130
|
const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
|
|
139
131
|
assert.equal(finals.length, 1, 'exactly one pushFinal reached the parent');
|
|
140
132
|
const body = reportBody(finals[0].ref);
|
|
141
|
-
assert.ok(body.includes('
|
|
133
|
+
assert.ok(body.includes('“yes, ship”'), 'the readable slot answer is inlined in the report');
|
|
142
134
|
assert.ok(body.includes(join(dir, 'response.json')));
|
|
143
135
|
});
|
|
144
136
|
test('a replay against a terminal bridge is a no-op', async () => {
|
|
145
137
|
const { parent, bridge } = bridgeUnder();
|
|
146
138
|
submitBridgePage(bridge);
|
|
147
|
-
await resolvePageTicket(bridge, { question: { text: 'yes', edited: true
|
|
139
|
+
await resolvePageTicket(bridge, { question: { text: 'yes', edited: true } });
|
|
148
140
|
assert.equal(await deliverTerminalResult(bridge), true);
|
|
149
141
|
closeDb();
|
|
150
142
|
const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
|
|
@@ -152,7 +144,7 @@ describe('terminal ticket delivery', () => {
|
|
|
152
144
|
});
|
|
153
145
|
test('cancelHumanTicket finishes the bridge and notifies non-actor subscribers', async () => {
|
|
154
146
|
const actor = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'actor' }).node_id;
|
|
155
|
-
const bridge = spawnNode({ kind: 'human', parent: actor, cwd: home, name: 'human-
|
|
147
|
+
const bridge = spawnNode({ kind: 'human', parent: actor, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
|
|
156
148
|
const other = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'other' }).node_id;
|
|
157
149
|
const { subscribe } = await import('../canvas/canvas.js');
|
|
158
150
|
subscribe(other, bridge, true);
|
|
@@ -162,15 +154,19 @@ describe('terminal ticket delivery', () => {
|
|
|
162
154
|
assert.equal(getNode(bridge)?.status, 'done');
|
|
163
155
|
assert.equal(readInboxSince(actor).length, 0, 'the canceling actor is not messaged');
|
|
164
156
|
const notes = readInboxSince(other);
|
|
165
|
-
assert.equal(notes.length, 1, 'the other waiter gets one
|
|
166
|
-
assert.equal(notes[0].tier, '
|
|
157
|
+
assert.equal(notes.length, 1, 'the other waiter gets one waking note');
|
|
158
|
+
assert.equal(notes[0].tier, 'normal');
|
|
167
159
|
assert.match(notes[0].label, /canceled/);
|
|
168
160
|
});
|
|
169
161
|
});
|
|
170
|
-
test('
|
|
162
|
+
test('a display-only page publishes passively and creates no canvas node', async () => {
|
|
163
|
+
const page = join(home, 'build-done.tsx');
|
|
164
|
+
writeFileSync(page, 'export default () => (\n <Page title="Build done" subtitle="The build passed, so the change is ready to review.">\n <p>Build output is ready.</p>\n </Page>\n);\n');
|
|
171
165
|
const before = listNodes().length;
|
|
172
|
-
const result = (await
|
|
173
|
-
assert.equal(result['
|
|
166
|
+
const result = (await humanSend.run({ page }));
|
|
167
|
+
assert.equal(result['reply'], false);
|
|
168
|
+
assert.equal(result['inbox'], false);
|
|
169
|
+
assert.equal(typeof result['page_id'], 'string');
|
|
174
170
|
assert.equal(typeof result['dir'], 'string');
|
|
175
171
|
assert.ok(existsSync(join(result['dir'], 'page.json')));
|
|
176
172
|
assert.ok(readTicketResult(result['dir']) === null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/human-node-not-supervised.test.ts
|
|
2
2
|
//
|
|
3
3
|
// BUG REGRESSION (issue #2 — "Headless node can't reach human-ask: human-kind
|
|
4
|
-
// TUI dies at boot"): a headless orchestrator's every `crtr human
|
|
4
|
+
// TUI dies at boot"): a headless orchestrator's every reply-bearing `crtr human send` bridge died
|
|
5
5
|
// at boot with "pi vehicle exited before the session came up".
|
|
6
6
|
//
|
|
7
7
|
// ROOT CAUSE: a kind:'human' row is NOT a broker-hosted agent node — it is a
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/plugin-page-components.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Wiring pins for plugin-contributed page components — the layer whose absence
|
|
4
|
+
// is SILENT: every authoring, validation, daemon, inbox, and TUI consumer takes
|
|
5
|
+
// the catalog as a parameter, so if `resolvePageComponents` stopped reading
|
|
6
|
+
// installed plugins' manifest `page_components` blocks, every plugin-shipped
|
|
7
|
+
// component would simply stop being authorable with all suites green.
|
|
8
|
+
// Plus the archive path: `validatePluginBundle` must carry a valid bundle.json
|
|
9
|
+
// `page_components` block into `ValidatedBundle.page_components` (what the
|
|
10
|
+
// installer copies into the synthesized manifest) and reject an invalid one.
|
|
11
|
+
import { test, before, beforeEach, afterEach, after } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, realpathSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import * as tar from 'tar';
|
|
17
|
+
import { resetScopeCache } from '../scope.js';
|
|
18
|
+
import { resolvePageComponents } from '../config.js';
|
|
19
|
+
import { validatePluginBundle } from '../command-plugins/bundle.js';
|
|
20
|
+
let isolatedHome;
|
|
21
|
+
let isolatedCwd;
|
|
22
|
+
let prevHome;
|
|
23
|
+
let prevCwd;
|
|
24
|
+
before(() => {
|
|
25
|
+
prevHome = process.env.HOME;
|
|
26
|
+
prevCwd = process.cwd();
|
|
27
|
+
});
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
isolatedHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-page-components-home-')));
|
|
30
|
+
isolatedCwd = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-page-components-cwd-')));
|
|
31
|
+
process.env.HOME = isolatedHome;
|
|
32
|
+
process.chdir(isolatedCwd);
|
|
33
|
+
resetScopeCache();
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
process.chdir(prevCwd);
|
|
37
|
+
rmSync(isolatedHome, { recursive: true, force: true });
|
|
38
|
+
rmSync(isolatedCwd, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
after(() => {
|
|
41
|
+
process.chdir(prevCwd);
|
|
42
|
+
process.env.HOME = prevHome;
|
|
43
|
+
resetScopeCache();
|
|
44
|
+
});
|
|
45
|
+
/** Plant a manifest-only plugin (page components need no commands or executable). */
|
|
46
|
+
function plantPlugin(scopeRootPath, name, pageComponents) {
|
|
47
|
+
const root = join(scopeRootPath, 'plugins', name, '.crouter-plugin');
|
|
48
|
+
mkdirSync(root, { recursive: true });
|
|
49
|
+
writeFileSync(join(root, 'plugin.json'), JSON.stringify({ name, version: '0.1.0', page_components: pageComponents }));
|
|
50
|
+
}
|
|
51
|
+
function userRoot() {
|
|
52
|
+
const root = join(isolatedHome, '.crouter');
|
|
53
|
+
mkdirSync(root, { recursive: true });
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
test('an enabled user-scope plugin contributes its page components; a disabled one contributes nothing', () => {
|
|
57
|
+
const root = userRoot();
|
|
58
|
+
plantPlugin(root, 'northlight', [
|
|
59
|
+
{ kind: 'connector', description: 'Connector decision', useWhen: 'Use for a connector decision.', doc: 'Product contract.' },
|
|
60
|
+
{ kind: 'applet-card', display: true },
|
|
61
|
+
]);
|
|
62
|
+
plantPlugin(root, 'zzz-disabled', ['ghost']);
|
|
63
|
+
writeFileSync(join(root, 'config.json'), JSON.stringify({ plugins: { 'zzz-disabled': { enabled: false } } }));
|
|
64
|
+
const components = resolvePageComponents();
|
|
65
|
+
assert.deepEqual(components, [
|
|
66
|
+
{ kind: 'connector', description: 'Connector decision', useWhen: 'Use for a connector decision.', doc: 'Product contract.' },
|
|
67
|
+
{ kind: 'applet-card', display: true },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
test('user config entries and plugin contributions resolve together, and a colliding kind is rejected', () => {
|
|
71
|
+
const root = userRoot();
|
|
72
|
+
plantPlugin(root, 'northlight', ['connector']);
|
|
73
|
+
writeFileSync(join(root, 'config.json'), JSON.stringify({ page_components: ['approval'] }));
|
|
74
|
+
assert.deepEqual(resolvePageComponents().map((component) => component.kind), ['approval', 'connector']);
|
|
75
|
+
writeFileSync(join(root, 'config.json'), JSON.stringify({ page_components: ['connector'] }));
|
|
76
|
+
assert.throws(() => resolvePageComponents(), /page component kind "connector" is registered by both config.json and plugin "northlight"/);
|
|
77
|
+
});
|
|
78
|
+
test('two plugins whose kinds derive one JSX tag are rejected, naming both sources', () => {
|
|
79
|
+
const root = userRoot();
|
|
80
|
+
plantPlugin(root, 'alpha', ['foo-bar']);
|
|
81
|
+
plantPlugin(root, 'beta', ['foo--bar']);
|
|
82
|
+
assert.throws(() => resolvePageComponents(), /kind "foo--bar" \(plugin "beta"\) derives JSX tag "FooBar", which duplicates kind "foo-bar" from plugin "alpha"/);
|
|
83
|
+
});
|
|
84
|
+
// --- archive path -----------------------------------------------------------
|
|
85
|
+
function minimalCommandsJson() {
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
mounts: [
|
|
89
|
+
{
|
|
90
|
+
parent: [],
|
|
91
|
+
node: {
|
|
92
|
+
kind: 'branch',
|
|
93
|
+
name: 'app',
|
|
94
|
+
description: 'application lifecycle and inspection',
|
|
95
|
+
whenToUse: 'you need to inspect an application',
|
|
96
|
+
rootEntry: {
|
|
97
|
+
concept: 'applications hosted by a crouter home',
|
|
98
|
+
description: 'application lifecycle and inspection',
|
|
99
|
+
whenToUse: 'you need to inspect an application',
|
|
100
|
+
},
|
|
101
|
+
summary: 'application lifecycle and inspection',
|
|
102
|
+
model: 'Applications are product resources owned by the selected home.',
|
|
103
|
+
children: [
|
|
104
|
+
{
|
|
105
|
+
kind: 'leaf',
|
|
106
|
+
name: 'show',
|
|
107
|
+
description: 'show one application',
|
|
108
|
+
whenToUse: 'inspect a single application by id',
|
|
109
|
+
tier: 'important',
|
|
110
|
+
summary: 'show one application details',
|
|
111
|
+
params: [{ kind: 'positional', name: 'app-id', required: true, constraint: 'the application id' }],
|
|
112
|
+
output: [{ name: 'app_id', type: 'string', required: true, constraint: 'the echoed application id' }],
|
|
113
|
+
effects: ['None. Read-only.'],
|
|
114
|
+
rest: { method: 'GET', path: '/v1/apps/{app-id}', params: { 'app-id': { in: 'path' } } },
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function makeArchive(bundleJson) {
|
|
123
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-page-components-tar-'));
|
|
124
|
+
try {
|
|
125
|
+
writeFileSync(join(dir, 'bundle.json'), JSON.stringify(bundleJson));
|
|
126
|
+
writeFileSync(join(dir, 'commands.json'), JSON.stringify(minimalCommandsJson()));
|
|
127
|
+
const pack = tar.create({ sync: true, cwd: dir, portable: true }, ['bundle.json', 'commands.json']);
|
|
128
|
+
const chunks = [];
|
|
129
|
+
let chunk;
|
|
130
|
+
while ((chunk = pack.read()) !== null)
|
|
131
|
+
chunks.push(chunk);
|
|
132
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
rmSync(dir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
test('validatePluginBundle carries a valid bundle.json page_components block into ValidatedBundle', async () => {
|
|
139
|
+
const archive = makeArchive({
|
|
140
|
+
bundleVersion: 1,
|
|
141
|
+
page_components: ['connector', { kind: 'applet-card', description: 'Applet card', display: true }],
|
|
142
|
+
});
|
|
143
|
+
const validated = await validatePluginBundle(archive, { reservedCoreNames: new Set(['node']) });
|
|
144
|
+
assert.deepEqual(validated.issues, []);
|
|
145
|
+
assert.deepEqual(validated.bundle?.page_components, [
|
|
146
|
+
{ kind: 'connector' },
|
|
147
|
+
{ kind: 'applet-card', description: 'Applet card', display: true },
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
test('validatePluginBundle rejects an invalid page_components block loudly', async () => {
|
|
151
|
+
const archive = makeArchive({ bundleVersion: 1, page_components: [{ kind: 'connector', use_when: 'typo field' }] });
|
|
152
|
+
const validated = await validatePluginBundle(archive, { reservedCoreNames: new Set(['node']) });
|
|
153
|
+
assert.equal(validated.bundle, undefined);
|
|
154
|
+
assert.equal(validated.issues.length, 1);
|
|
155
|
+
assert.match(validated.issues[0].message, /page_components block is invalid/);
|
|
156
|
+
assert.match(validated.issues[0].received, /use_when/);
|
|
157
|
+
});
|
|
@@ -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,22 @@
|
|
|
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';
|
|
22
|
+
import { writeFileSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
18
24
|
import { appendInbox, readCursor, readInboxSince } from '../../feed/inbox.js';
|
|
19
|
-
import { subscribe, updateNode } from '../../canvas/index.js';
|
|
25
|
+
import { registerWarmSpare, subscribe, updateNode } from '../../canvas/index.js';
|
|
20
26
|
import { isPidAlive } from '../../canvas/pid.js';
|
|
21
27
|
import { reviveNode } from '../../runtime/revive.js';
|
|
22
28
|
import { createHarness } from '../helpers/harness.js';
|
|
@@ -172,6 +178,99 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
172
178
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
173
179
|
}
|
|
174
180
|
});
|
|
181
|
+
test('a connected broker viewer keeps an obligation-free resident live', { timeout: 30_000 }, async () => {
|
|
182
|
+
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
183
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
184
|
+
const nodeId = h.fabricateBrokerNode({
|
|
185
|
+
id: 'settled-with-viewer',
|
|
186
|
+
parent: root,
|
|
187
|
+
lifecycle: 'resident',
|
|
188
|
+
status: 'idle',
|
|
189
|
+
intent: 'idle-release',
|
|
190
|
+
});
|
|
191
|
+
updateNode(nodeId, {
|
|
192
|
+
launch: {
|
|
193
|
+
extensions: [],
|
|
194
|
+
tools: [],
|
|
195
|
+
systemPrompt: '',
|
|
196
|
+
env: {},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
reviveNode(nodeId, { resume: true });
|
|
200
|
+
const boot = await h.awaitBoot(nodeId);
|
|
201
|
+
// This is the presence written by a helloed direct attach, which has no
|
|
202
|
+
// focuses-table row because it did not pass through the focus opener.
|
|
203
|
+
writeFileSync(join(h.home, 'nodes', nodeId, 'job', 'attach.json'), JSON.stringify({ viewers: 1 }));
|
|
204
|
+
try {
|
|
205
|
+
const seenAt = 15_000;
|
|
206
|
+
await h.tick(seenAt);
|
|
207
|
+
await h.tick(seenAt + 1500);
|
|
208
|
+
assert.equal(h.fleet.has(nodeId), true, 'an attached viewer keeps the broker fleet-owned');
|
|
209
|
+
assert.equal(isPidAlive(boot.pid), true, 'an attached viewer keeps the broker process alive');
|
|
210
|
+
assert.ok(['active', 'idle'].includes(h.node(nodeId)?.status ?? ''), 'an attached viewer keeps the node live');
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
if (priorParkMs === undefined)
|
|
214
|
+
delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
215
|
+
else
|
|
216
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
test('an already idle terminal node with no live obligation reconciles into canceled', async () => {
|
|
220
|
+
const nodeId = h.fabricateBrokerNode({
|
|
221
|
+
id: 'idle-terminal-without-obligation',
|
|
222
|
+
parent: root,
|
|
223
|
+
status: 'idle',
|
|
224
|
+
intent: 'idle-release',
|
|
225
|
+
pi_session_id: 'parked-terminal-session',
|
|
226
|
+
});
|
|
227
|
+
await h.tick();
|
|
228
|
+
assert.equal(h.node(nodeId)?.status, 'canceled', 'a terminal row that never pushed a final settles as torn down, not completed');
|
|
229
|
+
});
|
|
230
|
+
test('the unattended park clock cancels an obligation-free terminal node and spares the warm pool', { timeout: 40_000 }, async () => {
|
|
231
|
+
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
232
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
233
|
+
try {
|
|
234
|
+
const boot = async (id) => {
|
|
235
|
+
updateNode(id, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
236
|
+
reviveNode(id, { resume: true });
|
|
237
|
+
return (await h.awaitBoot(id)).pid;
|
|
238
|
+
};
|
|
239
|
+
const settled = h.fabricateBrokerNode({ id: 'terminal-no-work', parent: root, status: 'idle', intent: 'idle-release' });
|
|
240
|
+
const awaiting = h.fabricateBrokerNode({ id: 'terminal-awaiting-child', parent: root, status: 'idle', intent: 'idle-release' });
|
|
241
|
+
const child = h.fabricateBrokerNode({ parent: awaiting, status: 'idle', intent: 'idle-release' });
|
|
242
|
+
subscribe(awaiting, child, true);
|
|
243
|
+
// Production spares mint as unparented residents, so this is the row shape
|
|
244
|
+
// the park clock actually meets in the fleet.
|
|
245
|
+
const spare = h.fabricateBrokerNode({ id: 'unclaimed-warm-spare', lifecycle: 'resident', status: 'idle', intent: 'idle-release' });
|
|
246
|
+
registerWarmSpare(spare, 'test-recipe-key');
|
|
247
|
+
const settledPid = await boot(settled);
|
|
248
|
+
const awaitingPid = await boot(awaiting);
|
|
249
|
+
const sparePid = await boot(spare);
|
|
250
|
+
const seenAt = 30_000;
|
|
251
|
+
await h.tick(seenAt);
|
|
252
|
+
await h.tick(seenAt + 1500);
|
|
253
|
+
await h.waitFor(() => h.node(settled)?.status === 'canceled' && !isPidAlive(settledPid), {
|
|
254
|
+
timeoutMs: 10_000,
|
|
255
|
+
intervalMs: 20,
|
|
256
|
+
label: 'unattended terminal cancellation and broker exit',
|
|
257
|
+
});
|
|
258
|
+
await h.awaitFleetExit(settled);
|
|
259
|
+
assert.equal(h.node(settled)?.status, 'canceled', 'a terminal node torn down without a final is prunable canceled history');
|
|
260
|
+
await h.waitFor(() => !isPidAlive(awaitingPid), { timeoutMs: 10_000, intervalMs: 20, label: 'awaiting terminal release' });
|
|
261
|
+
await h.awaitFleetExit(awaiting);
|
|
262
|
+
assert.equal(h.node(awaiting)?.status, 'idle', 'a live child keeps a terminal node eligible for an automatic wake');
|
|
263
|
+
assert.equal(h.fleet.has(spare), true, 'the park clock never reaps an unclaimed spare out from under the pool');
|
|
264
|
+
assert.equal(isPidAlive(sparePid), true, 'the spare keeps the booted broker that is its whole value');
|
|
265
|
+
assert.ok(['active', 'idle'].includes(h.node(spare)?.status ?? ''), 'the spare row stays claimable, left for reapStaleSpares to own');
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
if (priorParkMs === undefined)
|
|
269
|
+
delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
270
|
+
else
|
|
271
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
175
274
|
test('an unattended resident awaiting a live child remains wakeable', { timeout: 30_000 }, async () => {
|
|
176
275
|
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
177
276
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
@@ -150,7 +150,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
150
150
|
// ('natural stop while awaiting a live worker → idle-release …').
|
|
151
151
|
// ===================================================================
|
|
152
152
|
{
|
|
153
|
-
const stop = evaluateStop(B, { pushedFinal: false,
|
|
153
|
+
const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
|
|
154
154
|
assert.equal(stop.action, 'allow', 'awaiting terminal B → allow (legit idle-release)');
|
|
155
155
|
assert.equal(stop.reason, 'awaiting', "B classified awaiting on its ACTIVE live sub to C");
|
|
156
156
|
}
|
|
@@ -225,7 +225,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
|
|
|
225
225
|
assert.deepEqual(drift?.from, { mode: 'base', lifecycle: 'terminal' }, 'drift.from = the acked base×terminal');
|
|
226
226
|
assert.deepEqual(drift?.to, { mode: 'orchestrator', lifecycle: 'terminal' }, 'drift.to = the new orchestrator persona');
|
|
227
227
|
assert.match(drift?.guidance ?? '', /ORCHESTRATOR/i, 'the steer carries the base→orchestrator guidance');
|
|
228
|
-
const stop = evaluateStop(B, { pushedFinal: false,
|
|
228
|
+
const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
|
|
229
229
|
assert.equal(stop.action, 'reprompt', 'orchestrator B with no live sub, no final → reprompt');
|
|
230
230
|
assert.equal(stop.reason, 'stalled', 'classified stalled');
|
|
231
231
|
assert.equal(stop.action === 'reprompt' ? stop.message : '', STALL_REPROMPT, 'the stall reprompt nudge');
|
|
@@ -11,6 +11,7 @@ import { ApiError, CrtrClient } from '../../../api/index.js';
|
|
|
11
11
|
import { createApiServer } from '../../../daemon/api/server.js';
|
|
12
12
|
import { apiSocketPath } from '../../canvas/paths.js';
|
|
13
13
|
import { claimTicket, readTicketClaim } from '../../human/claim.js';
|
|
14
|
+
import { bindTicketReplyRoute } from '../../human/convention.js';
|
|
14
15
|
import { ticketDir } from '../../human/root.js';
|
|
15
16
|
import { submitPage, submitReview } from '../../human/tickets.js';
|
|
16
17
|
import { spawnNode } from '../../runtime/nodes.js';
|
|
@@ -66,20 +67,14 @@ async function expectReviewSubmitGuidance(nodeId, body) {
|
|
|
66
67
|
function submitBridgePage(bridge) {
|
|
67
68
|
const dir = ticketDir(bridge);
|
|
68
69
|
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'));
|
|
70
|
+
const sourceFile = join(sourceDir, 'page.tsx');
|
|
71
|
+
writeFileSync(sourceFile, 'export default () => (\n <Page title="Ship it?" subtitle="Decide whether to ship now so the release can proceed.">\n <UserText id="question" initialText="" />\n </Page>\n);\n');
|
|
81
72
|
try {
|
|
82
|
-
|
|
73
|
+
bindTicketReplyRoute(dir, bridge);
|
|
74
|
+
// The delivery `human send` derives for a terminal bridge page: a
|
|
75
|
+
// response-bearing slot gives it a reply route, and a terminal sender's page
|
|
76
|
+
// always projects to the inbox.
|
|
77
|
+
submitPage({ dir, sourceFile, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
83
78
|
}
|
|
84
79
|
finally {
|
|
85
80
|
rmSync(sourceDir, { recursive: true, force: true });
|
|
@@ -110,6 +105,7 @@ test('malformed resolves leave the mounted ticket claim in place', async () => {
|
|
|
110
105
|
const reviewDir = ticketDir(reviewBridge);
|
|
111
106
|
const file = join(home, 'review.md');
|
|
112
107
|
writeFileSync(file, '# Review\n');
|
|
108
|
+
bindTicketReplyRoute(reviewDir, reviewBridge);
|
|
113
109
|
submitReview({ dir: reviewDir, review: { title: 'Review', subtitle: 'Review this file so implementation can proceed safely.', source: {}, file } });
|
|
114
110
|
const reviewClaim = claimTicket(reviewDir);
|
|
115
111
|
assert.ok(reviewClaim !== null);
|
|
@@ -118,10 +114,10 @@ test('malformed resolves leave the mounted ticket claim in place', async () => {
|
|
|
118
114
|
});
|
|
119
115
|
test('the ticket resolve route pushFinals the readable answer to the parent', async () => {
|
|
120
116
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
121
|
-
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-
|
|
117
|
+
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
|
|
122
118
|
const dir = submitBridgePage(bridge);
|
|
123
|
-
const result = await client.resolveHumanTicket(bridge, { responses: { question: { text: 'yes, ship', edited: true
|
|
124
|
-
assert.deepEqual(result, {
|
|
119
|
+
const result = await client.resolveHumanTicket(bridge, { responses: { question: { text: 'yes, ship', edited: true } } });
|
|
120
|
+
assert.deepEqual(result, { completed: true, reply_delivered: true, kind: 'page' });
|
|
125
121
|
assert.equal(getNode(bridge)?.status, 'done', 'bridge finalized by crtrd');
|
|
126
122
|
const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
|
|
127
123
|
assert.equal(finals.length, 1, 'exactly one pushFinal reached the parent');
|
|
@@ -48,7 +48,7 @@ import { evaluateStop } from '../../runtime/stop-guard.js';
|
|
|
48
48
|
function persona(m) {
|
|
49
49
|
return { mode: m.mode, lifecycle: m.lifecycle };
|
|
50
50
|
}
|
|
51
|
-
const SIGNALS = { pushedFinal: false,
|
|
51
|
+
const SIGNALS = { pushedFinal: false, requestedHumanReply: false };
|
|
52
52
|
// ===========================================================================
|
|
53
53
|
// (a) LIFECYCLE FLIP — `crtr node config --lifecycle` on a LIVE node, both directions,
|
|
54
54
|
// observing the idle-release DECISION change. A round-trip on ONE node
|
|
@@ -186,7 +186,7 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
186
186
|
const defaultMenu = tmuxBinding(socket, 'root', 'M-c') ?? '';
|
|
187
187
|
assert.match(defaultMenu, /display-menu/);
|
|
188
188
|
// The inbox toggle installs at its default key (alt+i → M-i) in the same sweep.
|
|
189
|
-
assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr
|
|
189
|
+
assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr surface inbox/);
|
|
190
190
|
const remapped = installTmuxBindings(resolveKeybindings({
|
|
191
191
|
'crtr.tmux.menu.open': ['alt+shift+z'],
|
|
192
192
|
'crtr.tmux.menu.graph': ['shift+z'],
|
|
@@ -242,11 +242,13 @@ test('tmux installer reads pair-array manifests and replaces owned pairs', (t) =
|
|
|
242
242
|
try {
|
|
243
243
|
// Current-scope releases wrote the pair array before wrappers were added.
|
|
244
244
|
isolatedTmux(socket, ['set-option', '-s', '@crtr_keybindings_manifest', JSON.stringify([{ table: 'root', key: 'M-i' }])]);
|
|
245
|
+
// Seeded with the pre-move command path, so the assertion below distinguishes a
|
|
246
|
+
// genuine replacement from a no-op.
|
|
245
247
|
isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-i', 'run-shell', '-b', 'crtr human inbox toggle --quiet']);
|
|
246
248
|
const result = installTmuxBindings(resolveKeybindings());
|
|
247
249
|
assert.equal(result.ok, true, JSON.stringify(result.diagnostics));
|
|
248
250
|
assert.ok(!result.diagnostics.some((d) => d.bindingId === 'crtr.tmux.inbox.toggle'), JSON.stringify(result.diagnostics));
|
|
249
|
-
assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr
|
|
251
|
+
assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr surface inbox/);
|
|
250
252
|
assert.match(tmuxBinding(socket, 'root', 'M-c') ?? '', /background running bash/);
|
|
251
253
|
}
|
|
252
254
|
finally {
|