@crouton-kit/crouter 0.3.28 → 0.3.29
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/build-root.js +4 -2
- package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +20 -18
- package/dist/builtin-memory/design.md +1 -1
- package/dist/builtin-memory/development.md +1 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- 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/planning.md +1 -1
- package/dist/builtin-memory/spec.md +6 -4
- package/dist/builtin-personas/orchestration-kernel.md +4 -5
- package/dist/builtin-personas/runtime-base.md +5 -4
- package/dist/builtin-personas/spec/PERSONA.md +8 -4
- package/dist/builtin-personas/spec/orchestrator.md +5 -3
- package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
- package/dist/builtin-views/canvas/core.mjs +586 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +165 -0
- package/dist/builtin-views/canvas/web.jsx +120 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +302 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +312 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +427 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.js +3 -3
- package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
- package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
- package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
- package/dist/clients/attach/__tests__/git-info.test.js +31 -0
- package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
- package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +282 -46
- package/dist/clients/attach/auth-pickers.d.ts +9 -0
- package/dist/clients/attach/auth-pickers.js +194 -0
- package/dist/clients/attach/canvas-panels.d.ts +4 -2
- package/dist/clients/attach/canvas-panels.js +45 -17
- package/dist/clients/attach/chat-view.d.ts +27 -2
- package/dist/clients/attach/chat-view.js +98 -9
- package/dist/clients/attach/clipboard-image.d.ts +47 -8
- package/dist/clients/attach/clipboard-image.js +150 -26
- package/dist/clients/attach/clipboard-text.d.ts +3 -0
- package/dist/clients/attach/clipboard-text.js +39 -0
- package/dist/clients/attach/config-load.d.ts +54 -7
- package/dist/clients/attach/config-load.js +99 -3
- package/dist/clients/attach/context-message.d.ts +12 -0
- package/dist/clients/attach/context-message.js +72 -0
- package/dist/clients/attach/git-info.d.ts +20 -0
- package/dist/clients/attach/git-info.js +44 -0
- package/dist/clients/attach/graph-overlay.d.ts +13 -4
- package/dist/clients/attach/graph-overlay.js +59 -19
- package/dist/clients/attach/input-controller.d.ts +75 -13
- package/dist/clients/attach/input-controller.js +305 -67
- package/dist/clients/attach/pickers.d.ts +58 -0
- package/dist/clients/attach/pickers.js +171 -0
- package/dist/clients/attach/slash-commands.d.ts +22 -0
- package/dist/clients/attach/slash-commands.js +91 -25
- package/dist/clients/attach/titled-editor.d.ts +33 -0
- package/dist/clients/attach/titled-editor.js +84 -0
- package/dist/clients/attach/view-socket.d.ts +18 -1
- package/dist/clients/attach/view-socket.js +70 -1
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +59 -0
- package/dist/clients/web/events.d.ts +14 -0
- package/dist/clients/web/events.js +151 -0
- package/dist/clients/web/server.d.ts +18 -0
- package/dist/clients/web/server.js +450 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +120 -0
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
- package/dist/commands/memory/lint.js +14 -2
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/memory/shared.d.ts +7 -1
- package/dist/commands/memory/shared.js +18 -3
- package/dist/commands/memory/write.js +8 -4
- package/dist/commands/memory.js +4 -4
- package/dist/commands/node.d.ts +1 -1
- package/dist/commands/node.js +64 -65
- package/dist/commands/pkg.js +1 -2
- package/dist/commands/revive.js +11 -3
- package/dist/commands/sys/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +3 -2
- package/dist/commands/view-cycle.js +2 -2
- package/dist/commands/view-list.js +8 -8
- package/dist/commands/view-new.js +21 -17
- package/dist/commands/view-pick.js +1 -1
- package/dist/commands/view-run.js +35 -14
- package/dist/commands/view.js +8 -6
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +161 -0
- package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
- package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
- package/dist/core/__tests__/child-death-wake.test.js +56 -111
- package/dist/core/__tests__/close.test.js +14 -20
- package/dist/core/__tests__/context-intro.test.js +19 -19
- package/dist/core/__tests__/daemon-boot.test.js +30 -23
- package/dist/core/__tests__/draw-style.test.js +1 -1
- package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +25 -13
- package/dist/core/__tests__/fork.test.js +22 -5
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
- package/dist/core/__tests__/full/cascade-close.test.js +22 -8
- package/dist/core/__tests__/full/detach-focus.test.js +66 -169
- package/dist/core/__tests__/helpers/harness.d.ts +6 -9
- package/dist/core/__tests__/helpers/harness.js +19 -36
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
- package/dist/core/__tests__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory-resolver.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/reset.test.js +19 -79
- package/dist/core/__tests__/steer-note.test.js +9 -7
- package/dist/core/__tests__/tmux-surface.test.js +13 -7
- package/dist/core/bootstrap.d.ts +0 -3
- package/dist/core/bootstrap.js +1 -143
- package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
- package/dist/core/canvas/browse/app.js +131 -8
- package/dist/core/canvas/browse/model.d.ts +23 -5
- package/dist/core/canvas/browse/model.js +68 -13
- package/dist/core/canvas/browse/render.d.ts +7 -1
- package/dist/core/canvas/browse/render.js +178 -47
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/focuses.d.ts +4 -4
- package/dist/core/canvas/focuses.js +7 -7
- package/dist/core/canvas/nav-model.js +8 -7
- package/dist/core/canvas/render.d.ts +68 -9
- package/dist/core/canvas/render.js +195 -61
- package/dist/core/canvas/types.d.ts +16 -23
- package/dist/core/config.js +1 -4
- package/dist/core/memory-resolver.js +43 -5
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +8 -8
- package/dist/core/runtime/branded-host.d.ts +16 -0
- package/dist/core/runtime/branded-host.js +127 -0
- package/dist/core/runtime/broker-protocol.d.ts +212 -6
- package/dist/core/runtime/broker.d.ts +4 -1
- package/dist/core/runtime/broker.js +494 -58
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +6 -14
- package/dist/core/runtime/host.d.ts +14 -34
- package/dist/core/runtime/host.js +14 -50
- package/dist/core/runtime/launch.d.ts +12 -9
- package/dist/core/runtime/launch.js +27 -15
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +15 -19
- package/dist/core/runtime/nodes.d.ts +0 -55
- package/dist/core/runtime/nodes.js +6 -74
- package/dist/core/runtime/placement.d.ts +91 -335
- package/dist/core/runtime/placement.js +262 -840
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +24 -6
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +107 -0
- package/dist/core/runtime/recycle.js +25 -61
- package/dist/core/runtime/reset.d.ts +43 -37
- package/dist/core/runtime/reset.js +131 -218
- package/dist/core/runtime/revive.d.ts +9 -29
- package/dist/core/runtime/revive.js +40 -139
- package/dist/core/runtime/spawn.d.ts +15 -16
- package/dist/core/runtime/spawn.js +165 -194
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux.d.ts +13 -50
- package/dist/core/runtime/tmux.js +33 -87
- package/dist/core/scope.d.ts +1 -2
- package/dist/core/scope.js +1 -5
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
- package/dist/core/skill-sync/builtins.d.ts +42 -0
- package/dist/core/skill-sync/builtins.js +112 -0
- package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
- package/dist/core/skill-sync/claude-plugins.js +71 -0
- package/dist/core/skill-sync/engine.d.ts +42 -0
- package/dist/core/skill-sync/engine.js +633 -0
- package/dist/core/skill-sync/export.d.ts +23 -0
- package/dist/core/skill-sync/export.js +86 -0
- package/dist/core/skill-sync/manifest.d.ts +64 -0
- package/dist/core/skill-sync/manifest.js +181 -0
- package/dist/core/skill-sync/profile.d.ts +76 -0
- package/dist/core/skill-sync/profile.js +173 -0
- package/dist/core/skill-sync/snapshot.d.ts +57 -0
- package/dist/core/skill-sync/snapshot.js +120 -0
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.js +3 -3
- package/dist/core/substrate/on-read.js +68 -6
- package/dist/core/substrate/render.d.ts +8 -11
- package/dist/core/substrate/render.js +29 -43
- package/dist/core/substrate/schema.d.ts +10 -3
- package/dist/core/substrate/schema.js +6 -3
- package/dist/core/tui/host.d.ts +12 -4
- package/dist/core/tui/host.js +280 -149
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +171 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +70 -0
- package/dist/core/view/transport.d.ts +4 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/daemon/crtrd.d.ts +38 -27
- package/dist/daemon/crtrd.js +303 -376
- package/dist/daemon/manage.js +6 -1
- package/dist/index.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
- package/dist/pi-extensions/canvas-context-intro.js +4 -4
- package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
- package/dist/pi-extensions/canvas-nav.js +5 -0
- package/dist/pi-extensions/canvas-recap.d.ts +37 -0
- package/dist/pi-extensions/canvas-recap.js +236 -0
- package/dist/pi-extensions/canvas-resume.js +2 -1
- package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
- package/dist/pi-extensions/canvas-stophook.js +87 -101
- package/dist/pi-extensions/canvas-view.js +2 -1
- package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
- package/dist/pi-extensions/widget-order-bus.js +34 -0
- package/dist/prompts/view.d.ts +2 -2
- package/dist/prompts/view.js +145 -73
- package/dist/types.d.ts +1 -6
- package/dist/types.js +1 -3
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +16 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +133 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +28 -0
- package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
- package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
- package/dist/web-client/index.html +13 -0
- package/package.json +20 -6
- package/dist/builtin-views/canvas/client.mjs +0 -303
- package/dist/builtin-views/canvas/view.mjs +0 -576
- package/dist/builtin-views/git-pr/client.mjs +0 -440
- package/dist/builtin-views/git-pr/view.mjs +0 -675
- package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
- package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
- package/dist/builtin-views/inbox/view.mjs +0 -889
- package/dist/builtin-views/linkedin/client.mjs +0 -610
- package/dist/builtin-views/linkedin/view.mjs +0 -1171
- package/dist/commands/pkg/bridge.d.ts +0 -1
- package/dist/commands/pkg/bridge.js +0 -137
- package/dist/commands/skill/author.d.ts +0 -3
- package/dist/commands/skill/author.js +0 -140
- package/dist/commands/skill/shared.d.ts +0 -3
- package/dist/commands/skill/shared.js +0 -19
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.js +0 -21
- package/dist/commands/tmux-spread.d.ts +0 -2
- package/dist/commands/tmux-spread.js +0 -144
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
- package/dist/core/__tests__/full/placement-focus.test.js +0 -309
- package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
- package/dist/core/__tests__/full/placement-revive.test.js +0 -238
- package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
- package/dist/core/__tests__/home-session.test.js +0 -186
- package/dist/core/__tests__/relaunch.test.js +0 -335
- package/dist/core/bridge-map.d.ts +0 -19
- package/dist/core/bridge-map.js +0 -73
- package/dist/core/tui/contract.d.ts +0 -83
- package/dist/core/tui/contract.js +0 -8
- package/dist/core/tui/loader.d.ts +0 -16
- package/dist/core/tui/loader.js +0 -94
- package/dist/prompts/skill.d.ts +0 -2
- package/dist/prompts/skill.js +0 -650
- /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
- /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// workspace.ts — `crtr workspace enter`: the agent-centric editor layout.
|
|
2
|
+
//
|
|
3
|
+
// Turns the current tmux window into an editor view for attached headless nodes:
|
|
4
|
+
// a narrow LEFT rail (the `workspace-sidebar` view — this graph + other live
|
|
5
|
+
// agents in this cwd, with ⚑ inbox flags) beside a CHAT pane (`crtr attach`).
|
|
6
|
+
// Selecting a node in the rail swaps it into the chat pane.
|
|
7
|
+
//
|
|
8
|
+
// Layout build (the only two driver acts, both through placement's seam):
|
|
9
|
+
// 1. splitWindow(here, before+size) → the rail pane, left of here, runs
|
|
10
|
+
// `crtr view run workspace-sidebar`.
|
|
11
|
+
// 2. `crtr node focus <node> --pane <here>` → swaps the chat node's attach
|
|
12
|
+
// viewer into here's slot (here's old content relocates to the backstage).
|
|
13
|
+
// The rail finds the chat pane by DISCOVERY (the `@crtr_node` tag attach sets),
|
|
14
|
+
// so it is robust to that swap — no launch-time pane id is threaded into it.
|
|
15
|
+
//
|
|
16
|
+
// Command shape: the path-walker forbids a flat top-level leaf (same constraint
|
|
17
|
+
// as attach), so this is a BRANCH wrapping one leaf: `crtr workspace enter`.
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
20
|
+
import { InputError } from '../core/io.js';
|
|
21
|
+
import { getNode, listNodes } from '../core/canvas/index.js';
|
|
22
|
+
import { inTmux, currentTmux, splitWindow, piCommand } from '../core/runtime/placement.js';
|
|
23
|
+
const RAIL_WIDTH = 32;
|
|
24
|
+
/** Newest LIVE (active|idle) forest root started in `cwd` — the default node the
|
|
25
|
+
* chat pane attaches to when `--node` is omitted. A root has no parent inside
|
|
26
|
+
* the cwd-scoped set. Returns null when this cwd has no live root. */
|
|
27
|
+
function defaultChatNode(cwd) {
|
|
28
|
+
const rows = listNodes({ status: ['active', 'idle'] }).filter((r) => r.cwd === cwd);
|
|
29
|
+
if (rows.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
const ids = new Set(rows.map((r) => r.node_id));
|
|
32
|
+
const roots = rows.filter((r) => r.parent === null || !ids.has(r.parent));
|
|
33
|
+
const pool = roots.length > 0 ? roots : rows;
|
|
34
|
+
pool.sort((a, b) => (a.created < b.created ? 1 : -1)); // newest first
|
|
35
|
+
return pool[0].node_id;
|
|
36
|
+
}
|
|
37
|
+
const workspaceEnterLeaf = defineLeaf({
|
|
38
|
+
name: 'enter',
|
|
39
|
+
description: 'enter the editor view — a node rail beside a chat pane, in this window',
|
|
40
|
+
whenToUse: 'you want to work in the agent-centric editor layout: a left rail listing this graph + the other live agents in this working directory (with ⚑ inbox flags) beside a chat pane attached to a node. Selecting a node in the rail swaps it into the chat pane. Opens in the CURRENT tmux window. tmux-only.',
|
|
41
|
+
help: {
|
|
42
|
+
name: 'workspace enter',
|
|
43
|
+
summary: 'split the current window into a left node-rail (the workspace-sidebar view) + a chat pane (crtr attach); ↵ in the rail swaps a node into the chat. tmux-only.',
|
|
44
|
+
params: [
|
|
45
|
+
{
|
|
46
|
+
kind: 'flag',
|
|
47
|
+
name: 'node',
|
|
48
|
+
type: 'string',
|
|
49
|
+
required: false,
|
|
50
|
+
constraint: 'Node to attach in the chat pane. Default: the newest live root in this cwd (rail-only, empty chat, if none).',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
kind: 'flag',
|
|
54
|
+
name: 'width',
|
|
55
|
+
type: 'int',
|
|
56
|
+
required: false,
|
|
57
|
+
default: RAIL_WIDTH,
|
|
58
|
+
constraint: `Rail width in columns (default ${RAIL_WIDTH}).`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
output: [
|
|
62
|
+
{ name: 'rail', type: 'string', required: false, constraint: 'tmux pane id of the rail.' },
|
|
63
|
+
{ name: 'chat', type: 'string', required: false, constraint: 'node id attached in the chat pane (absent if none).' },
|
|
64
|
+
{ name: 'note', type: 'string', required: false, constraint: 'Set only on the non-TTY/piped path.' },
|
|
65
|
+
],
|
|
66
|
+
outputKind: 'object',
|
|
67
|
+
effects: [
|
|
68
|
+
'Splits a narrow LEFT pane in the current window running the workspace-sidebar view.',
|
|
69
|
+
'Swaps the chosen (or default) node into the current pane as a chat (crtr node focus), relocating the current pane to the backstage.',
|
|
70
|
+
'Outside a TTY/tmux: prints a notice and exits 0 (no layout change).',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
run: async (input) => {
|
|
74
|
+
const node = input['node'];
|
|
75
|
+
const width = input['width'] ?? RAIL_WIDTH;
|
|
76
|
+
if (!process.stdout.isTTY) {
|
|
77
|
+
return { note: 'crtr workspace enter builds a tmux layout — run it in a tmux pane (a TTY), not a pipe.' };
|
|
78
|
+
}
|
|
79
|
+
if (!inTmux()) {
|
|
80
|
+
throw new InputError({
|
|
81
|
+
error: 'not_in_tmux',
|
|
82
|
+
message: 'crtr workspace is tmux-only — run it inside the crtr tmux session',
|
|
83
|
+
next: 'Open it from inside tmux.',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const here = currentTmux();
|
|
87
|
+
if (here === null) {
|
|
88
|
+
throw new InputError({
|
|
89
|
+
error: 'no_tmux_location',
|
|
90
|
+
message: 'could not resolve the current tmux pane',
|
|
91
|
+
next: 'Run this inside a tmux pane.',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Resolve the chat node (explicit --node, else newest live root here).
|
|
95
|
+
let chatNode = null;
|
|
96
|
+
if (node !== undefined && node !== '') {
|
|
97
|
+
if (getNode(node) === null) {
|
|
98
|
+
throw new InputError({
|
|
99
|
+
error: 'not_found',
|
|
100
|
+
message: `no node: ${node}`,
|
|
101
|
+
received: node,
|
|
102
|
+
next: 'List nodes with `crtr node inspect list`.',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
chatNode = node;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
chatNode = defaultChatNode(process.cwd());
|
|
109
|
+
}
|
|
110
|
+
// 1. The rail — a narrow split to the LEFT of here. It discovers the chat
|
|
111
|
+
// pane itself (the @crtr_node tag), so no pane id is threaded in.
|
|
112
|
+
const railCmd = piCommand(['view', 'run', 'workspace-sidebar', '--target', here.pane], 'crtr');
|
|
113
|
+
const rail = splitWindow(here.pane, {
|
|
114
|
+
cwd: process.cwd(),
|
|
115
|
+
env: {},
|
|
116
|
+
command: railCmd,
|
|
117
|
+
before: true,
|
|
118
|
+
size: Math.max(16, width),
|
|
119
|
+
});
|
|
120
|
+
if (rail === null) {
|
|
121
|
+
throw new InputError({
|
|
122
|
+
error: 'split_failed',
|
|
123
|
+
message: 'tmux could not split the rail pane',
|
|
124
|
+
next: 'Check the tmux server is reachable, then retry.',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// 2. The chat — swap the node's attach viewer into here's slot. Best-effort:
|
|
128
|
+
// the rail is already up, so a focus failure degrades to a rail-only view
|
|
129
|
+
// the user can drive (↵ a node) rather than aborting the whole layout.
|
|
130
|
+
if (chatNode !== null) {
|
|
131
|
+
await new Promise((resolve) => {
|
|
132
|
+
execFile('crtr', ['node', 'focus', chatNode, '--pane', here.pane], () => resolve());
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return chatNode !== null ? { rail, chat: chatNode } : { rail };
|
|
136
|
+
},
|
|
137
|
+
render: (result) => {
|
|
138
|
+
if (result['note'] !== undefined)
|
|
139
|
+
return String(result['note']);
|
|
140
|
+
const chat = result['chat'];
|
|
141
|
+
return chat !== undefined
|
|
142
|
+
? `Workspace ready — rail ${result['rail']}, chat attached to ${chat}.`
|
|
143
|
+
: `Workspace ready — rail ${result['rail']}. No live agent here yet; press ↵ on a node in the rail to open one.`;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
export function registerWorkspace() {
|
|
147
|
+
return defineBranch({
|
|
148
|
+
name: 'workspace',
|
|
149
|
+
rootEntry: {
|
|
150
|
+
concept: 'the agent-centric editor view — a node rail beside a chat pane, in one tmux window',
|
|
151
|
+
desc: 'enter the editor layout for attached headless nodes',
|
|
152
|
+
useWhen: 'you want an editor-style workspace in tmux: a left rail of this graph + other live agents in this cwd (with ⚑ inbox flags) next to a chat pane attached to a node, where selecting a node in the rail swaps it into the chat. tmux-only.',
|
|
153
|
+
},
|
|
154
|
+
help: {
|
|
155
|
+
name: 'workspace',
|
|
156
|
+
summary: 'enter the agent-centric editor view (node rail + chat pane) in the current tmux window',
|
|
157
|
+
model: '`enter` splits the current window into a narrow left rail (the workspace-sidebar view — this graph + other live agents in this cwd, ⚑ inbox flags) and a chat pane (`crtr attach`) attached to a node (--node, or the newest live root here). ↵ in the rail swaps the selected node into the chat pane. tmux-only; piped it prints a notice and exits 0.',
|
|
158
|
+
},
|
|
159
|
+
children: [workspaceEnterLeaf],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/broker-double-spawn.test.ts
|
|
2
|
+
//
|
|
3
|
+
// BUG-REGRESSION (broker-universal-host cut, design §C "double-spawn verdict";
|
|
4
|
+
// taste/broker-is-the-host). Two coupled invariants the cut introduced:
|
|
5
|
+
//
|
|
6
|
+
// 1. A pre-cut row carries host_kind='tmux' (or null). While its RAW pi pid is
|
|
7
|
+
// still ALIVE the daemon must NEVER relaunch it — a relaunch would put a
|
|
8
|
+
// SECOND vehicle on the same .jsonl (the double-spawn). Liveness is the
|
|
9
|
+
// recorded engine pid alone; an alive pid is always 'leave'.
|
|
10
|
+
// 2. When that pid dies and the node revives, it comes back as a BROKER and the
|
|
11
|
+
// row's host_kind is lazily COERCED tmux→'broker' (reviveNode, revive.ts
|
|
12
|
+
// ~91-97) so inspect/history read honest values and the daemon never
|
|
13
|
+
// branches it back to a tmux host.
|
|
14
|
+
//
|
|
15
|
+
// Three legs, strongest→cheapest: the pure verdict (D1a), the direct reviveNode
|
|
16
|
+
// coerce (D1b), and the full daemon integration over a real live→dead pid (D1c).
|
|
17
|
+
// Regression check: if the cut's coerce is removed, D1b/D1c go RED (host_kind
|
|
18
|
+
// stays 'tmux'); if an alive pid is ever revived, D1c's "no relaunch" asserts go
|
|
19
|
+
// RED (cycles bumps / host_kind flips while the pid is still alive).
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
23
|
+
import { createHeadlessHarness } from './helpers/harness.js';
|
|
24
|
+
import { createNode } from '../canvas/canvas.js';
|
|
25
|
+
import { closeDb } from '../canvas/db.js';
|
|
26
|
+
import { reviveNode } from '../runtime/revive.js';
|
|
27
|
+
import { livenessVerdict, isPidAlive } from '../../daemon/crtrd.js';
|
|
28
|
+
// A pid reaped (dead) by the time spawnSync returns — the "dead but supervised"
|
|
29
|
+
// pid the grace path judges.
|
|
30
|
+
function deadPid() {
|
|
31
|
+
const r = spawnSync('true', [], { stdio: 'ignore' });
|
|
32
|
+
return r.pid ?? 0x7ffffffe;
|
|
33
|
+
}
|
|
34
|
+
/** Fabricate a PRE-CUT host_kind='tmux' row directly (the harness's
|
|
35
|
+
* fabricateBrokerNode hardcodes host_kind:'broker', so we mirror its body here
|
|
36
|
+
* with host_kind:'tmux' — no harness edit). The result is a row the daemon
|
|
37
|
+
* supervises by pid exactly as a legacy tmux node would be. */
|
|
38
|
+
function fabricateTmuxNode(o) {
|
|
39
|
+
const meta = {
|
|
40
|
+
node_id: o.id,
|
|
41
|
+
name: o.id,
|
|
42
|
+
created: new Date().toISOString(),
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
host_kind: 'tmux',
|
|
45
|
+
kind: 'developer',
|
|
46
|
+
mode: 'base',
|
|
47
|
+
lifecycle: 'terminal',
|
|
48
|
+
parent: null,
|
|
49
|
+
pi_session_id: o.pi_session_id,
|
|
50
|
+
status: 'active',
|
|
51
|
+
intent: null,
|
|
52
|
+
pi_pid: o.pi_pid,
|
|
53
|
+
};
|
|
54
|
+
createNode(meta);
|
|
55
|
+
closeDb();
|
|
56
|
+
return o.id;
|
|
57
|
+
}
|
|
58
|
+
// REVIVE_GRACE_MS is 20_000 (crtrd.ts); pick offsets well inside / well past it.
|
|
59
|
+
const T_ALIVE = 6_000_000;
|
|
60
|
+
const T_DEAD = 6_100_000; // first dead observation
|
|
61
|
+
const T_PAST = T_DEAD + 25_000; // > 20s after first-observed-dead → revive
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// D1a — pure verdict: an alive pid is NEVER relaunched (no double-spawn).
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
test('D1a livenessVerdict: an ALIVE pid is left alone (never a double-spawn); a DEAD pid pends then revives', () => {
|
|
66
|
+
assert.equal(livenessVerdict(true, null), 'leave', 'alive pid, first obs → leave');
|
|
67
|
+
assert.equal(livenessVerdict(true, 10_000_000), 'leave', 'alive pid is NEVER relaunched');
|
|
68
|
+
assert.equal(livenessVerdict(false, null), 'pending', 'dead, first obs → pending (double-spawn guard)');
|
|
69
|
+
assert.equal(livenessVerdict(false, 1_000), 'pending', 'dead, within grace → pending');
|
|
70
|
+
assert.equal(livenessVerdict(false, 10_000_000), 'revive', 'dead past grace → revive');
|
|
71
|
+
});
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// D1b — direct reviveNode coerce: a tmux row flips to 'broker' on revive.
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
test('D1b reviveNode coerces a pre-cut host_kind=tmux row to broker', async () => {
|
|
76
|
+
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-d1b' });
|
|
77
|
+
try {
|
|
78
|
+
const id = fabricateTmuxNode({ id: 'd1b-node', pi_pid: deadPid(), pi_session_id: 'sess-d1b' });
|
|
79
|
+
assert.equal(h.node(id).host_kind, 'tmux', 'precondition: row is a legacy tmux host');
|
|
80
|
+
// reviveNode's lazy coerce runs at the top, before any launch (revive.ts ~91-97).
|
|
81
|
+
reviveNode(id, { resume: true });
|
|
82
|
+
assert.equal(h.node(id).host_kind, 'broker', 'tmux→broker coerced on revive (the daemon never branches it back)');
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await h.dispose();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// D1c — full integration: an ALIVE tmux pid is NOT relaunched; once it dies the
|
|
90
|
+
// node revives as a broker (host_kind coerced, cycles bumped).
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
test('D1c daemon: a live tmux pi is never double-spawned; on death it revives as a broker', { timeout: 30_000 }, async () => {
|
|
93
|
+
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-d1c' });
|
|
94
|
+
// A real live subprocess whose pid stands in for the raw pi vehicle.
|
|
95
|
+
const child = spawn('sleep', ['300'], { stdio: 'ignore' });
|
|
96
|
+
const livePid = child.pid;
|
|
97
|
+
try {
|
|
98
|
+
assert.equal(isPidAlive(livePid), true, 'precondition: the raw pi pid is alive');
|
|
99
|
+
// pi_session_id SET so the dead-pid path routes the crash/grace revive, NOT
|
|
100
|
+
// the never-booted boot-failure branch.
|
|
101
|
+
const id = fabricateTmuxNode({ id: 'd1c-node', pi_pid: livePid, pi_session_id: 'sess-d1c' });
|
|
102
|
+
assert.equal(h.node(id).host_kind, 'tmux', 'precondition: pre-cut tmux host');
|
|
103
|
+
assert.equal(h.node(id).cycles ?? 0, 0, 'no revive yet');
|
|
104
|
+
// --- TICK while the pi is ALIVE: must be a no-op. The daemon never
|
|
105
|
+
// relaunches a live pid, so reviveNode is never called → NO coerce, NO
|
|
106
|
+
// cycle bump, pid unchanged. This is the double-spawn guard. ---
|
|
107
|
+
await h.tick(T_ALIVE);
|
|
108
|
+
{
|
|
109
|
+
const n = h.node(id);
|
|
110
|
+
assert.equal(n.host_kind, 'tmux', 'live pid: host_kind NOT coerced (reviveNode never ran → no double-spawn)');
|
|
111
|
+
assert.equal(n.cycles ?? 0, 0, 'live pid: NO revive (cycles still 0)');
|
|
112
|
+
assert.equal(n.pi_pid, livePid, 'live pid: pi_pid unchanged — no second vehicle launched');
|
|
113
|
+
}
|
|
114
|
+
// Kill the raw pi and wait until the daemon would observe it dead.
|
|
115
|
+
child.kill('SIGKILL');
|
|
116
|
+
await h.waitFor(() => (isPidAlive(livePid) ? null : true), { label: 'raw pi dead', timeoutMs: 10_000 });
|
|
117
|
+
// --- TICK: first dead observation → pending (within grace). No revive. ---
|
|
118
|
+
await h.tick(T_DEAD);
|
|
119
|
+
{
|
|
120
|
+
const n = h.node(id);
|
|
121
|
+
assert.equal(n.cycles ?? 0, 0, 'first-observed-dead: pends, no revive yet');
|
|
122
|
+
assert.equal(n.host_kind, 'tmux', 'still tmux until the revive actually fires');
|
|
123
|
+
}
|
|
124
|
+
// --- TICK past the grace: the crash/grace revive fires → reviveNode coerces
|
|
125
|
+
// host_kind tmux→broker and bumps cycles (observable instantly, the
|
|
126
|
+
// model effect lands before the detached broker spawn — grace-clock). ---
|
|
127
|
+
await h.tick(T_PAST);
|
|
128
|
+
{
|
|
129
|
+
const n = h.node(id);
|
|
130
|
+
assert.equal(n.cycles ?? 0, 1, 'past grace: revived → cycles bumped to 1');
|
|
131
|
+
assert.equal(n.host_kind, 'broker', 'past grace: host_kind coerced tmux→broker on revive');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
try {
|
|
136
|
+
child.kill('SIGKILL');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* already gone */
|
|
140
|
+
}
|
|
141
|
+
await h.dispose();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/broker-fork-seam.test.ts
|
|
2
|
+
//
|
|
3
|
+
// BUG-REGRESSION (broker-universal-host cut, design §B.broker; U1 report
|
|
4
|
+
// mq8ftenr-5d55313d). Before the cut, the headless broker had NO fork path — its
|
|
5
|
+
// session selection THREW `--fork is not supported by the headless broker`. After
|
|
6
|
+
// the cut EVERY node is a broker, so `node new --fork-from <id>` would always hit
|
|
7
|
+
// that throw. The fix routes the broker's fork through pi's REAL fork seam,
|
|
8
|
+
// SessionManager.forkFrom (broker.ts buildBrokerSession, the `forking` branch) —
|
|
9
|
+
// the same method pi's own `--fork` CLI flag uses.
|
|
10
|
+
//
|
|
11
|
+
// This test drives the BROKER's OWN fork branch (broker.ts buildBrokerSession,
|
|
12
|
+
// the `forking` branch ~1106) end to end: it calls `buildBrokerSession` with
|
|
13
|
+
// `cfg.forkFrom` set to a real source session .jsonl — the EXACT path a
|
|
14
|
+
// `node new --fork-from <id>` boot takes through the broker — and asserts the
|
|
15
|
+
// resulting live session is a genuine fork: a NEW session id (not a colliding
|
|
16
|
+
// copy) and a `parentSession` header pointing at the source, with the source's
|
|
17
|
+
// history copied in. Because it invokes `buildBrokerSession` (not
|
|
18
|
+
// `SessionManager.forkFrom` directly), it goes RED if broker.ts's fork branch
|
|
19
|
+
// reverts to `throw "--fork is not supported by the headless broker"` — it
|
|
20
|
+
// guards its own stated regression.
|
|
21
|
+
//
|
|
22
|
+
// Like the C3/C4 real-SDK wiring tests, it drives the REAL engine (broker-sdk's
|
|
23
|
+
// static SDK re-exports) — NOT the CRTR_BROKER_ENGINE fake the lifecycle suite
|
|
24
|
+
// uses, whose fixture omits forkFrom and so could never exercise this branch. It
|
|
25
|
+
// runs offline (fork + session/services assembly are all local: no network/auth).
|
|
26
|
+
//
|
|
27
|
+
// (fork.test.ts covers the pure argv/resolve layers — buildPiArgv emitting
|
|
28
|
+
// `--fork`, resolveForkSource turning a ref into a path. THIS file covers the
|
|
29
|
+
// broker runtime branch those layers feed.)
|
|
30
|
+
import { test, before, after } from 'node:test';
|
|
31
|
+
import assert from 'node:assert/strict';
|
|
32
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { tmpdir } from 'node:os';
|
|
34
|
+
import { join } from 'node:path';
|
|
35
|
+
import { createAgentSessionServices, createAgentSessionFromServices, SessionManager, VERSION, } from '../runtime/broker-sdk.js';
|
|
36
|
+
import { buildBrokerSession } from '../runtime/broker.js';
|
|
37
|
+
// The REAL engine — bypasses the CRTR_BROKER_ENGINE fake entirely (the whole
|
|
38
|
+
// point: the fake omits forkFrom, so only the real SDK can prove the branch).
|
|
39
|
+
const realEngine = {
|
|
40
|
+
createAgentSessionServices,
|
|
41
|
+
createAgentSessionFromServices,
|
|
42
|
+
SessionManager,
|
|
43
|
+
VERSION,
|
|
44
|
+
};
|
|
45
|
+
let dir;
|
|
46
|
+
before(() => {
|
|
47
|
+
dir = mkdtempSync(join(tmpdir(), 'crtr-fork-seam-'));
|
|
48
|
+
});
|
|
49
|
+
after(() => {
|
|
50
|
+
rmSync(dir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
/** A minimal but VALID pi session .jsonl (the exact on-disk shape v3 emits): a
|
|
53
|
+
* `session` header line + two `message` entries. This is what a node that has
|
|
54
|
+
* run a turn or two carries on disk — the source the broker forks from. */
|
|
55
|
+
function writeSourceSession(path, srcId) {
|
|
56
|
+
const lines = [
|
|
57
|
+
JSON.stringify({ type: 'session', version: 3, id: srcId, timestamp: '2026-06-10T20:27:32.891Z', cwd: dir }),
|
|
58
|
+
JSON.stringify({ type: 'message', id: 'a42170e1', parentId: null, timestamp: '2026-06-10T20:27:32.893Z', message: { role: 'user', content: 'source turn one' } }),
|
|
59
|
+
JSON.stringify({ type: 'message', id: 'f309b932', parentId: 'a42170e1', timestamp: '2026-06-10T20:27:32.893Z', message: { role: 'assistant', content: 'source reply one' } }),
|
|
60
|
+
];
|
|
61
|
+
writeFileSync(path, lines.join('\n') + '\n');
|
|
62
|
+
return 2; // entry count (the two message lines)
|
|
63
|
+
}
|
|
64
|
+
test('D2 broker fork branch: buildBrokerSession(cfg.forkFrom) yields a NEW id + parentSession header, copying history (never throws "fork not supported")', async () => {
|
|
65
|
+
// 1. A real source session on disk.
|
|
66
|
+
const srcFile = join(dir, 'src.jsonl');
|
|
67
|
+
const srcId = '019eb338-029b-7536-b3eb-d33bd6f641a8';
|
|
68
|
+
const srcEntryCount = writeSourceSession(srcFile, srcId);
|
|
69
|
+
// 2. Drive the BROKER's fork branch: buildBrokerSession with cfg.forkFrom set —
|
|
70
|
+
// the same recipe a `node new --fork-from <id>` boot produces. The OLD broker
|
|
71
|
+
// THREW in this branch ("--fork is not supported by the headless broker"), so
|
|
72
|
+
// if broker.ts:~1106 reverts to that throw, this rejects and the test goes RED.
|
|
73
|
+
const cfg = { cwd: dir, extensionPaths: [], forkFrom: srcFile };
|
|
74
|
+
let session;
|
|
75
|
+
let resuming;
|
|
76
|
+
await assert.doesNotReject(async () => {
|
|
77
|
+
const built = await buildBrokerSession(realEngine, cfg);
|
|
78
|
+
session = built.session;
|
|
79
|
+
resuming = built.resuming;
|
|
80
|
+
}, 'the broker fork branch must NOT throw "fork is not supported by the headless broker"');
|
|
81
|
+
assert.ok(session !== undefined, 'buildBrokerSession returned a live session for the fork');
|
|
82
|
+
try {
|
|
83
|
+
// 3. A fork boots as a FRESH spawn (resuming=false) so the kickoff firstPrompt
|
|
84
|
+
// fires — the new node gets the source history AND a new task to start on.
|
|
85
|
+
assert.equal(resuming, false, 'a fork is a fresh spawn (resuming=false) so the kickoff fires');
|
|
86
|
+
// 4. Real fork metadata on the broker's selected SessionManager, not a
|
|
87
|
+
// colliding .jsonl copy.
|
|
88
|
+
const sm = session.sessionManager;
|
|
89
|
+
assert.notEqual(sm.getSessionId(), srcId, 'the fork is a NEW session id (not a copy that would collide on resume)');
|
|
90
|
+
const header = sm.getHeader();
|
|
91
|
+
assert.equal(header?.parentSession, srcFile, 'the fork header records parentSession pointing at the source .jsonl');
|
|
92
|
+
// 5. The source history is carried into the fork (fork copies, not empty
|
|
93
|
+
// start). The broker boot may append its own session-start entry, so the
|
|
94
|
+
// fork holds AT LEAST the source's messages — a naïve empty session would
|
|
95
|
+
// have zero.
|
|
96
|
+
assert.ok(sm.getEntries().length >= srcEntryCount, `the fork carries the source conversation history forward ` +
|
|
97
|
+
`(>= ${srcEntryCount} source entries; got ${sm.getEntries().length})`);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
session.dispose();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
@@ -107,6 +107,7 @@ test('C2 — zero-viewer UI context resolves dialogs to deny/cancel immediately
|
|
|
107
107
|
throw new Error('C2: must NOT forward a dialog when no controller is attached');
|
|
108
108
|
},
|
|
109
109
|
pending: new Map(),
|
|
110
|
+
broadcast: () => { },
|
|
110
111
|
});
|
|
111
112
|
// confirm() WITH a large timeout: the fix must resolve at once (deny), NOT after
|
|
112
113
|
// the timeout — the old timeout-reliant path would have waited 60s (or, with no
|
|
@@ -144,6 +145,7 @@ test('M2 — a forwarded dialog stays pending on controller detach, resolving on
|
|
|
144
145
|
},
|
|
145
146
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
147
|
pending: pending,
|
|
148
|
+
broadcast: () => { },
|
|
147
149
|
});
|
|
148
150
|
// Controller attached → confirm() forwards + registers a pending dialog (with the
|
|
149
151
|
// request retained for welcome.pending_dialog / re-route, T4) and does NOT
|