@crouton-kit/crouter 0.3.18 → 0.3.19
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 +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +3 -2
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +2 -0
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +607 -61
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/loader.d.ts +7 -0
- package/dist/core/personas/loader.js +36 -5
- package/dist/core/personas/resolve.js +13 -3
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +32 -14
- package/dist/core/runtime/placement.js +206 -52
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +5 -0
- package/dist/core/spawn.js +20 -2
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +87 -23
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
// to by the agent being closed", generalized to any depth via a fixpoint.
|
|
31
31
|
import { getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
|
|
32
32
|
import { transition } from './lifecycle.js';
|
|
33
|
-
import {
|
|
33
|
+
import { hostFor } from './host.js';
|
|
34
34
|
import { appendInbox } from '../feed/inbox.js';
|
|
35
|
+
import { appendPassive } from '../feed/passive.js';
|
|
35
36
|
/** The set of nodes to close: the root plus every descendant reachable down the
|
|
36
37
|
* subscriptions spine, all of whose managers are themselves in the set. Grown
|
|
37
38
|
* to a fixpoint — a node added this pass can qualify its own children next
|
|
@@ -128,11 +129,13 @@ export function closeNode(rootId) {
|
|
|
128
129
|
.filter((c) => closing.has(c));
|
|
129
130
|
// 1) Canceled + intent cleared BEFORE the window dies (daemon race).
|
|
130
131
|
transition(id, 'cancel');
|
|
131
|
-
// 2) Tear the node
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
132
|
+
// 2) Tear the node's ENGINE down through the Host seam (hostFor): a tmux
|
|
133
|
+
// node runs tearDownNode (close any focus row it occupies, kill its
|
|
134
|
+
// PANE — the window closes once its last pane goes, so sibling nodes the
|
|
135
|
+
// user co-located in one window survive — and null its LOCATION); a
|
|
136
|
+
// broker node sends the `shutdown` frame so the broker PROCESS exits and
|
|
137
|
+
// releases the sole .jsonl writer (a paneless node has no pane to kill).
|
|
138
|
+
hostFor(m).teardown(id);
|
|
136
139
|
// 3) Leave the resume notice AFTER the watcher is gone, so it survives.
|
|
137
140
|
appendInbox(id, {
|
|
138
141
|
from: null,
|
|
@@ -141,6 +144,32 @@ export function closeNode(rootId) {
|
|
|
141
144
|
label: cancellationLabel(id === rootId, deadChildren),
|
|
142
145
|
data: { reason: 'user-close', cascade_root: rootId, canceled_children: deadChildren },
|
|
143
146
|
});
|
|
147
|
+
// 4) Wake any SURVIVING manager subscribed to this node — the doctrine wake.
|
|
148
|
+
// A node going dormant trusts the runtime to wake it on a child's
|
|
149
|
+
// terminal outcome; D-1 found that `node close` notified ONLY the closed
|
|
150
|
+
// node itself, never its subscribers, so a parent that delegated then
|
|
151
|
+
// just stopped hangs forever when its child is closed out from under it.
|
|
152
|
+
// Only the close ROOT can have a surviving subscriber (closingSet adds a
|
|
153
|
+
// non-root node only when EVERY subscriber is itself closing), so this
|
|
154
|
+
// reaches the still-living manager(s) of a deliberately-closed child and
|
|
155
|
+
// never fires inside a self-contained cascade. Active → inbox (wakes a
|
|
156
|
+
// dormant manager via its live watcher / the daemon's dormant-revive
|
|
157
|
+
// second pass); passive → passive accumulator (delivered, not woken).
|
|
158
|
+
for (const sub of subscribersOf(id)) {
|
|
159
|
+
if (closing.has(sub.node_id))
|
|
160
|
+
continue; // also being torn down — pointless
|
|
161
|
+
const notice = {
|
|
162
|
+
from: id,
|
|
163
|
+
tier: 'normal',
|
|
164
|
+
kind: 'message',
|
|
165
|
+
label: `Child closed — ${m.name} (${id}) was closed from the canvas and is no longer running.`,
|
|
166
|
+
data: { reason: 'child-closed', child: id },
|
|
167
|
+
};
|
|
168
|
+
if (sub.active)
|
|
169
|
+
appendInbox(sub.node_id, notice);
|
|
170
|
+
else
|
|
171
|
+
appendPassive(sub.node_id, notice);
|
|
172
|
+
}
|
|
144
173
|
closed.push(id);
|
|
145
174
|
}
|
|
146
175
|
catch {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type NodeMeta, type NodeRow } from '../canvas/index.js';
|
|
2
|
+
import type { PiInvocation } from './launch.js';
|
|
3
|
+
/** View-side launch hints the legacy host needs beyond the PiInvocation. This is
|
|
4
|
+
* today's ReviveLaunch (placement.ts) MINUS `command`/`env` — the host derives
|
|
5
|
+
* `command = piCommand(inv.argv)` and `env = inv.env` from the PiInvocation
|
|
6
|
+
* itself. The broker (Phase 3) uses only `cwd` + `resuming`. */
|
|
7
|
+
export interface LaunchPlacement {
|
|
8
|
+
cwd: string;
|
|
9
|
+
name: string;
|
|
10
|
+
resuming: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** A handle to a node's running engine container.
|
|
13
|
+
* §3.2 specifies { kind, pid }; Phase 1 carries the placement coords too,
|
|
14
|
+
* because launch REPLACES reviveIntoPlacement (which returned PlacementResult)
|
|
15
|
+
* and reviveNode's unchanged ReviveResult is built from window+session. The
|
|
16
|
+
* daemon never reads window/session/pane (it supervises via pid / isAlive); the
|
|
17
|
+
* broker (Phase 3) returns window=null, pane=null. */
|
|
18
|
+
export interface HostHandle {
|
|
19
|
+
kind: 'tmux' | 'broker';
|
|
20
|
+
/** Supervised pid (signal-0 target). null at launch for TmuxPaneHost: the
|
|
21
|
+
* pane's fresh pi records its own pid via the stophook (session_start →
|
|
22
|
+
* recordPid), and reviveNode clearPid()s right after launch — so no Phase-1
|
|
23
|
+
* caller reads this. Present for the §3.2 contract + the daemon. */
|
|
24
|
+
pid: number | null;
|
|
25
|
+
window: string | null;
|
|
26
|
+
session: string;
|
|
27
|
+
pane: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface Host {
|
|
30
|
+
/** Bring a node's ENGINE into existence from its launch recipe; return a
|
|
31
|
+
* supervisable handle. TmuxPaneHost ALSO performs placement (the pane IS the
|
|
32
|
+
* engine container). */
|
|
33
|
+
launch(nodeId: string, inv: PiInvocation, opts: LaunchPlacement): HostHandle;
|
|
34
|
+
/** Is this node's engine container present? PURE / non-mutating. §3.2 types
|
|
35
|
+
* this `NodeRow`; we keep `string | NodeRow` to match isNodePaneAlive (which
|
|
36
|
+
* it IS) so both call sites — the guard (passes the id) and the Phase-2 daemon
|
|
37
|
+
* (passes a row) — stay one-token changes. */
|
|
38
|
+
isAlive(node: string | NodeRow): boolean;
|
|
39
|
+
/** Tear the engine down (close/cancel teardown). */
|
|
40
|
+
teardown(nodeId: string): void;
|
|
41
|
+
/** Deliver an OS signal to the engine container — present so the daemon never
|
|
42
|
+
* reaches around the abstraction. No Phase-1 call site. */
|
|
43
|
+
signal(nodeId: string, sig: NodeJS.Signals): void;
|
|
44
|
+
}
|
|
45
|
+
/** The sole Phase-1 host: today's behavior, wrapped. Stateless → a frozen
|
|
46
|
+
* singleton, no class needed. Every method delegates verbatim. */
|
|
47
|
+
export declare const tmuxPaneHost: Host;
|
|
48
|
+
export declare const headlessBrokerHost: Host;
|
|
49
|
+
/** Select the Host for a node by its persisted `host_kind` (NULL/'tmux' → the
|
|
50
|
+
* tmux host; 'broker' → the headless broker host). The param is `NodeMeta |
|
|
51
|
+
* NodeRow` so both call sites — reviveNode `hostFor(meta)` and the daemon
|
|
52
|
+
* `hostFor(row)` — share one selector. */
|
|
53
|
+
export declare function hostFor(node: NodeMeta | NodeRow): Host;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// src/core/runtime/host.ts
|
|
2
|
+
//
|
|
3
|
+
// The Host abstraction (design §3): owns an engine PROCESS's lifecycle, not its
|
|
4
|
+
// display. TWO impls live here: TmuxPaneHost — a thin wrapper over today's
|
|
5
|
+
// placement functions (the pane IS the engine container) — and, since Phase 3,
|
|
6
|
+
// HeadlessBrokerHost — a detached broker process with no tmux pane. `host_kind`
|
|
7
|
+
// (on the node row/meta since Wave 1) selects between them via hostFor().
|
|
8
|
+
//
|
|
9
|
+
// IMPORT-LINT (tmux-surface.test.ts §5.1): this file must NOT import './tmux.js'
|
|
10
|
+
// directly. It reaches every driver verb (piCommand, reviveIntoPlacement,
|
|
11
|
+
// isNodePaneAlive, tearDownNode) through placement.ts's re-exports — placement.ts
|
|
12
|
+
// and tmux-chrome.ts are the only sanctioned tmux.ts importers. The broker host
|
|
13
|
+
// needs no tmux verb at all.
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { mkdirSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync } from 'node:fs';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { connect } from 'node:net';
|
|
19
|
+
import { reviveIntoPlacement, isNodePaneAlive, tearDownNode, piCommand, } from './placement.js';
|
|
20
|
+
import { getNode } from '../canvas/index.js';
|
|
21
|
+
import { nodeDir, jobDir } from '../canvas/paths.js';
|
|
22
|
+
import { encodeFrame } from './broker-protocol.js';
|
|
23
|
+
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
24
|
+
import { isPidAlive } from '../canvas/pid.js';
|
|
25
|
+
/** The sole Phase-1 host: today's behavior, wrapped. Stateless → a frozen
|
|
26
|
+
* singleton, no class needed. Every method delegates verbatim. */
|
|
27
|
+
export const tmuxPaneHost = {
|
|
28
|
+
launch(nodeId, inv, opts) {
|
|
29
|
+
const placed = reviveIntoPlacement(nodeId, {
|
|
30
|
+
command: piCommand(inv.argv),
|
|
31
|
+
env: inv.env,
|
|
32
|
+
cwd: opts.cwd,
|
|
33
|
+
name: opts.name,
|
|
34
|
+
resuming: opts.resuming,
|
|
35
|
+
});
|
|
36
|
+
return { kind: 'tmux', pid: null, window: placed.window, session: placed.session, pane: placed.pane };
|
|
37
|
+
},
|
|
38
|
+
isAlive(node) {
|
|
39
|
+
return isNodePaneAlive(node);
|
|
40
|
+
},
|
|
41
|
+
teardown(nodeId) {
|
|
42
|
+
tearDownNode(nodeId);
|
|
43
|
+
},
|
|
44
|
+
signal(nodeId, sig) {
|
|
45
|
+
const pid = getNode(nodeId)?.pi_pid;
|
|
46
|
+
if (pid != null) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, sig);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* already gone */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// HeadlessBrokerHost (design §3.3): a detached broker PROCESS, no tmux pane. It
|
|
58
|
+
// boots the dedicated broker-cli entry directly (NEVER src/cli.ts), supervises
|
|
59
|
+
// via the broker pid recorded as pi_pid, and tears down over the node's unix
|
|
60
|
+
// socket (graceful `shutdown` frame, SIGTERM fallback).
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
/** Resolve the absolute path to the broker entry. At runtime this file is
|
|
63
|
+
* dist/core/runtime/host.js; the entry lives at dist/core/runtime/broker-cli.js
|
|
64
|
+
* (sibling in the same directory) — the manage.ts/resolveCrtrdEntry pattern. */
|
|
65
|
+
function resolveBrokerEntry() {
|
|
66
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
return join(here, 'broker-cli.js');
|
|
68
|
+
}
|
|
69
|
+
/** How long teardown waits after sending the graceful `shutdown` frame before
|
|
70
|
+
* SIGTERMing a broker that connected but never exited (e.g. a hung dispose()). */
|
|
71
|
+
const BROKER_SHUTDOWN_GRACE_MS = 2_000;
|
|
72
|
+
export const headlessBrokerHost = {
|
|
73
|
+
launch(nodeId, inv, opts) {
|
|
74
|
+
const dir = nodeDir(nodeId);
|
|
75
|
+
mkdirSync(dir, { recursive: true });
|
|
76
|
+
// The broker reads this recipe back via runBroker(nodeId) → broker-launch.json.
|
|
77
|
+
writeFileSync(join(dir, 'broker-launch.json'), JSON.stringify(inv));
|
|
78
|
+
// Redirect the detached broker's stdout+stderr to a per-node log under the
|
|
79
|
+
// node's existing job/ dir (alongside log.jsonl / telemetry.json). Without
|
|
80
|
+
// this (stdio:'ignore') EVERY broker diagnostic — the version-skew warning,
|
|
81
|
+
// model-not-found, socket errors, first-prompt failure, and broker-cli's
|
|
82
|
+
// fatal-crash stack — goes to /dev/null, which is exactly what makes a boot
|
|
83
|
+
// failure invisible (review M-2). Append-mode so a crash-revive keeps history.
|
|
84
|
+
const logDir = jobDir(nodeId);
|
|
85
|
+
mkdirSync(logDir, { recursive: true });
|
|
86
|
+
const logFd = openSync(join(logDir, 'broker.log'), 'a');
|
|
87
|
+
const child = spawn(process.execPath, [resolveBrokerEntry(), nodeId], {
|
|
88
|
+
cwd: opts.cwd,
|
|
89
|
+
detached: true,
|
|
90
|
+
stdio: ['ignore', logFd, logFd],
|
|
91
|
+
env: { ...process.env, ...inv.env, [FRONT_DOOR_ENV]: '1' },
|
|
92
|
+
});
|
|
93
|
+
// The child holds its own dup of the fd; release the parent's copy so the
|
|
94
|
+
// launching process (CLI or daemon) never leaks it.
|
|
95
|
+
closeSync(logFd);
|
|
96
|
+
child.unref();
|
|
97
|
+
return { kind: 'broker', pid: child.pid ?? null, window: null, session: '', pane: null };
|
|
98
|
+
},
|
|
99
|
+
isAlive(node) {
|
|
100
|
+
return isPidAlive((typeof node === 'string' ? getNode(node) : node)?.pi_pid);
|
|
101
|
+
},
|
|
102
|
+
teardown(nodeId) {
|
|
103
|
+
// Graceful: connect to view.sock + send a `shutdown` frame → the broker
|
|
104
|
+
// dispose()s the engine, unlinks the socket, exits 0. Status is already
|
|
105
|
+
// flipped done/canceled by the caller (crash-safe ordering), so the daemon
|
|
106
|
+
// won't revive. On connect failure (broker dead/crashed) fall back to a
|
|
107
|
+
// SIGTERM of the broker pid + unlink the stale socket.
|
|
108
|
+
const sockPath = join(nodeDir(nodeId), 'view.sock');
|
|
109
|
+
const cleanupSocket = () => {
|
|
110
|
+
try {
|
|
111
|
+
if (existsSync(sockPath))
|
|
112
|
+
unlinkSync(sockPath);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* best-effort cleanup */
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const sock = connect(sockPath);
|
|
119
|
+
let connected = false;
|
|
120
|
+
sock.on('error', () => {
|
|
121
|
+
// A post-connect error (broker exiting after we sent shutdown) is benign
|
|
122
|
+
// and swallowed; only a connect failure triggers the SIGTERM fallback.
|
|
123
|
+
if (connected)
|
|
124
|
+
return;
|
|
125
|
+
const pid = getNode(nodeId)?.pi_pid;
|
|
126
|
+
if (pid != null) {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(pid, 'SIGTERM');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* already gone */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
cleanupSocket();
|
|
135
|
+
});
|
|
136
|
+
sock.once('connect', () => {
|
|
137
|
+
connected = true;
|
|
138
|
+
try {
|
|
139
|
+
sock.write(encodeFrame({ type: 'shutdown' }));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* the broker may have raced us to exit */
|
|
143
|
+
}
|
|
144
|
+
sock.end();
|
|
145
|
+
cleanupSocket();
|
|
146
|
+
// Bounded exit confirmation (review Mn-3): a broker that connects fine but
|
|
147
|
+
// then HANGS inside session.dispose() (disposeAndExit catches a throw, not
|
|
148
|
+
// a hang) would leak the process holding the sole .jsonl writer. Arm a
|
|
149
|
+
// short UNREF'd timer; if the captured pid is still alive when it fires,
|
|
150
|
+
// SIGTERM it. unref'd so the happy path adds NO latency — a short-lived CLI
|
|
151
|
+
// caller's loop is kept alive only by the still-open socket, i.e. precisely
|
|
152
|
+
// the hung case where the fallback must run.
|
|
153
|
+
const pid = getNode(nodeId)?.pi_pid;
|
|
154
|
+
if (pid != null) {
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
if (isPidAlive(pid)) {
|
|
157
|
+
try {
|
|
158
|
+
process.kill(pid, 'SIGTERM');
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
/* already gone */
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}, BROKER_SHUTDOWN_GRACE_MS).unref();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
signal(nodeId, sig) {
|
|
169
|
+
const pid = getNode(nodeId)?.pi_pid;
|
|
170
|
+
if (pid != null) {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, sig);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* already gone */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
/** Select the Host for a node by its persisted `host_kind` (NULL/'tmux' → the
|
|
181
|
+
* tmux host; 'broker' → the headless broker host). The param is `NodeMeta |
|
|
182
|
+
* NodeRow` so both call sites — reviveNode `hostFor(meta)` and the daemon
|
|
183
|
+
* `hostFor(row)` — share one selector. */
|
|
184
|
+
export function hostFor(node) {
|
|
185
|
+
return node.host_kind === 'broker' ? headlessBrokerHost : tmuxPaneHost;
|
|
186
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type NodeMeta } from '../canvas/index.js';
|
|
2
|
+
import { type WakeOrigin } from './bearings.js';
|
|
2
3
|
/** The goal file — the prompt/task a node was spawned with, persisted at birth
|
|
3
4
|
* so a fresh revive can re-read its mandate. */
|
|
4
5
|
export declare function goalPath(nodeId: string): string;
|
|
@@ -46,4 +47,4 @@ export declare function drainBearings(meta: NodeMeta): ReviveBearings;
|
|
|
46
47
|
* node's goal, roadmap, and context dir, framed so the revived node can rebuild
|
|
47
48
|
* its bearings in one turn. PURE: no state mutation, so calling it twice yields
|
|
48
49
|
* the same string and consumes nothing — drainBearings owns the one-shot reads. */
|
|
49
|
-
export declare function buildReviveKickoff(meta: NodeMeta, bearings: ReviveBearings): string;
|
|
50
|
+
export declare function buildReviveKickoff(meta: NodeMeta, bearings: ReviveBearings, wakeReason?: WakeOrigin): string;
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
// yield-message is one-shot (consumed on the next revive).
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
|
-
import {
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
import { contextDir, reportsDir, getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
|
|
23
24
|
import { readRoadmap, roadmapPath } from './roadmap.js';
|
|
25
|
+
import { buildWakeBearings } from './bearings.js';
|
|
24
26
|
import { personaDrift, commitPersonaAck } from './persona.js';
|
|
25
27
|
import { readInboxSince, readCursor, writeCursor, coalesce, } from '../feed/inbox.js';
|
|
26
28
|
// ---------------------------------------------------------------------------
|
|
@@ -126,8 +128,60 @@ export function drainBearings(meta) {
|
|
|
126
128
|
}
|
|
127
129
|
return { yieldMsg, unreadDigest, driftGuidance };
|
|
128
130
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
// Fresh-revive catch-up bug: on a refresh-yield (resume:false) the old
|
|
132
|
+
// conversation is gone AND the monotonic inbox cursor has already advanced past
|
|
133
|
+
// everything drained pre-yield, so the revived node loses sight of reports its
|
|
134
|
+
// subscriptions pushed BEFORE the yield. The bodies are never lost — they persist
|
|
135
|
+
// at reports/<ts>-<kind>.md forever — but nothing pointed the revived node at
|
|
136
|
+
// them. reportHistoryLines renders those existing on-disk paths so it can catch
|
|
137
|
+
// up; PATHS ONLY (the node dereferences what it needs), no body read, no parse.
|
|
138
|
+
const REPORT_HISTORY_PER_SOURCE = 5;
|
|
139
|
+
const REPORT_HISTORY_TOTAL_CAP = 20;
|
|
140
|
+
/** Collapse the home-dir prefix of an absolute path to a leading `~`, so report
|
|
141
|
+
* paths render as `~/.crouter/canvas/nodes/<id>/reports/<ts>.md` instead of
|
|
142
|
+
* repeating the long absolute home prefix on every catch-up line — cheaper
|
|
143
|
+
* context, still dereferenceable by the revived node. A path NOT under the home
|
|
144
|
+
* dir (e.g. a CRTR_HOME / project-scope home elsewhere) is returned unchanged,
|
|
145
|
+
* so we never mangle a non-home path. */
|
|
146
|
+
function tildify(p) {
|
|
147
|
+
const home = homedir();
|
|
148
|
+
return p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
149
|
+
}
|
|
150
|
+
/** Recent report file PATHS (most-recent-first), grouped by publisher, for each
|
|
151
|
+
* ACTIVE subscription of `nodeId` — independent of publisher liveness, so a
|
|
152
|
+
* finished worker's history still surfaces for catch-up. Skips subscriptions
|
|
153
|
+
* whose reports dir is empty/missing. Returns [] when the node has no active
|
|
154
|
+
* subscriptions or none have reports yet (caller renders nothing extra). */
|
|
155
|
+
function reportHistoryLines(nodeId) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
let total = 0;
|
|
158
|
+
for (const sub of subscriptionsOf(nodeId).filter((s) => s.active)) {
|
|
159
|
+
if (total >= REPORT_HISTORY_TOTAL_CAP)
|
|
160
|
+
break;
|
|
161
|
+
const dir = reportsDir(sub.node_id);
|
|
162
|
+
if (!existsSync(dir))
|
|
163
|
+
continue;
|
|
164
|
+
const files = readdirSync(dir)
|
|
165
|
+
.filter((f) => f.endsWith('.md'))
|
|
166
|
+
.sort() // YYYYMMDDTHHmmss-<kind>.md — lexical sort = chronological
|
|
167
|
+
.reverse() // most recent first
|
|
168
|
+
.slice(0, REPORT_HISTORY_PER_SOURCE);
|
|
169
|
+
if (files.length === 0)
|
|
170
|
+
continue;
|
|
171
|
+
const pub = getNode(sub.node_id);
|
|
172
|
+
lines.push(` ${pub !== null ? `${pub.name} (${sub.node_id})` : sub.node_id}:`);
|
|
173
|
+
for (const f of files) {
|
|
174
|
+
if (total >= REPORT_HISTORY_TOTAL_CAP)
|
|
175
|
+
break;
|
|
176
|
+
lines.push(` ${tildify(join(dir, f))}`);
|
|
177
|
+
total++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
/** Render the <feed> block PURELY: the live "awaiting" roster (a read), the
|
|
183
|
+
* already-drained unread digest (from drainBearings), and the on-disk report
|
|
184
|
+
* history of this node's subscriptions (catch-up pointers). No cursor write. */
|
|
131
185
|
function feedBlock(nodeId, unreadDigest) {
|
|
132
186
|
// Awaiting = active subscriptions whose publisher is still live (active|idle).
|
|
133
187
|
const awaiting = subscriptionsOf(nodeId)
|
|
@@ -154,6 +208,17 @@ function feedBlock(nodeId, unreadDigest) {
|
|
|
154
208
|
lines.push('Awaiting 0 nodes.');
|
|
155
209
|
lines.push('', unreadDigest ?? '(no unread reports)');
|
|
156
210
|
}
|
|
211
|
+
// Catch-up history. The unread digest above only covers what arrived since the
|
|
212
|
+
// cursor; on a refresh-revive the cursor has already passed everything drained
|
|
213
|
+
// pre-yield, so point the node at the durable report history that persists on
|
|
214
|
+
// disk. Renders nothing when the node has no active subscriptions with reports.
|
|
215
|
+
const history = reportHistoryLines(nodeId);
|
|
216
|
+
if (history.length > 0) {
|
|
217
|
+
lines.push('', 'Report history on disk — the nodes you subscribe to keep every push they made; ' +
|
|
218
|
+
'these persist across your context refresh (the unread digest above only covers ' +
|
|
219
|
+
'what arrived since your cursor). Most recent first; dereference any you need. ' +
|
|
220
|
+
'`crtr feed read --all` replays the full inbox history, cursor-independent.', ...history);
|
|
221
|
+
}
|
|
157
222
|
return `<feed>\n${lines.join('\n')}\n</feed>`;
|
|
158
223
|
}
|
|
159
224
|
// ---------------------------------------------------------------------------
|
|
@@ -164,19 +229,40 @@ function feedBlock(nodeId, unreadDigest) {
|
|
|
164
229
|
* node's goal, roadmap, and context dir, framed so the revived node can rebuild
|
|
165
230
|
* its bearings in one turn. PURE: no state mutation, so calling it twice yields
|
|
166
231
|
* the same string and consumes nothing — drainBearings owns the one-shot reads. */
|
|
167
|
-
export function buildReviveKickoff(meta, bearings) {
|
|
232
|
+
export function buildReviveKickoff(meta, bearings, wakeReason) {
|
|
168
233
|
const nodeId = meta.node_id;
|
|
169
234
|
const parts = [
|
|
170
235
|
`${REVIVE_KICKOFF_SENTINEL} — your previous in-memory ` +
|
|
171
236
|
'context is gone, by design. Everything below was just read from disk; it is your ' +
|
|
172
237
|
'full bearings. Rebuild from it and continue toward your goal.',
|
|
173
238
|
];
|
|
239
|
+
// Wake provenance (Invariant B/D): when a scheduled bare self-alarm fired this
|
|
240
|
+
// revive, the <crtr-wake> block reframes the generic "you were revived" above
|
|
241
|
+
// into "a TIMER woke you" — placed right after the sentinel (so the kickoff
|
|
242
|
+
// still STARTS with REVIVE_KICKOFF_SENTINEL, which goal-capture keys on) and
|
|
243
|
+
// before the roadmap/disk bearings, so "why you woke" precedes "what to rebuild
|
|
244
|
+
// from". Only the daemon's bare-wake branch passes wakeReason.
|
|
245
|
+
if (wakeReason !== undefined)
|
|
246
|
+
parts.push(buildWakeBearings(wakeReason));
|
|
174
247
|
// The roadmap is the source of truth on a fresh revive: its frozen core holds
|
|
175
248
|
// the goal/exit criteria, its body the live plan the node kept current. The
|
|
176
249
|
// original spawn prompt (context/initial-prompt.md) is deliberately NOT injected
|
|
177
250
|
// — it lives on disk only as a log, and by now it is usually stale.
|
|
178
251
|
const roadmap = readRoadmap(nodeId);
|
|
179
|
-
|
|
252
|
+
const hasRoadmapBody = roadmap !== null && roadmap.trim() !== '';
|
|
253
|
+
parts.push(`<roadmap file="${roadmapPath(nodeId)}">\n${hasRoadmapBody ? roadmap.trim() : '(no roadmap on disk yet)'}\n</roadmap>`);
|
|
254
|
+
// With NO roadmap the kickoff above carries no mandate at all, and an
|
|
255
|
+
// amnesiac fresh boot just stops (and is auto-finalized by the stop-guard) —
|
|
256
|
+
// observed when the daemon relaunches a spawned-but-never-launched node
|
|
257
|
+
// (audit 2026-06-09, Bug 4: mq45b6ch-9ecc2f03), whose only mandate on disk IS
|
|
258
|
+
// the goal. Surface the goal then. When a roadmap exists it stays the sole
|
|
259
|
+
// source of truth (the goal is usually stale by comparison) — unchanged.
|
|
260
|
+
if (!hasRoadmapBody) {
|
|
261
|
+
const goal = readGoal(nodeId);
|
|
262
|
+
if (goal !== null && goal.trim() !== '') {
|
|
263
|
+
parts.push(`<goal file="${goalPath(nodeId)}">\n${goal.trim()}\n</goal>`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
180
266
|
const files = listContextDir(nodeId);
|
|
181
267
|
parts.push(`<context-dir path="${contextDir(nodeId)}">\n${files.length > 0 ? files.join('\n') : '(empty)'}\n</context-dir>`);
|
|
182
268
|
parts.push(feedBlock(nodeId, bearings.unreadDigest));
|
|
@@ -5,15 +5,20 @@ export declare const CANVAS_NAV_PATH: string;
|
|
|
5
5
|
export declare const CANVAS_GOAL_CAPTURE_PATH: string;
|
|
6
6
|
export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
|
|
7
7
|
export declare const CANVAS_CONTEXT_INTRO_PATH: string;
|
|
8
|
+
export declare const CANVAS_DOC_SUBSTRATE_PATH: string;
|
|
8
9
|
export declare const CANVAS_COMMANDS_PATH: string;
|
|
9
10
|
export declare const CANVAS_RESUME_PATH: string;
|
|
11
|
+
export declare const CANVAS_VIEW_PATH: string;
|
|
10
12
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
11
13
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
12
14
|
* graph chrome), goal-capture (persist the first user message as the goal),
|
|
13
15
|
* passive-context (drain passive backlog as pre-text on the next message),
|
|
14
16
|
* context-intro (inject the <crtr-context> bearings block as its own session
|
|
15
|
-
* message, once per brand-new chat),
|
|
16
|
-
*
|
|
17
|
+
* message, once per brand-new chat), doc-substrate (the unified document
|
|
18
|
+
* substrate's two hooks: ## Skills + ## Preferences at boot, on-read context
|
|
19
|
+
* injection), commands (the /promote slash-command), resume (the /resume-node
|
|
20
|
+
* whole-canvas picker → `crtr node focus`), view (the /view popup → `crtr
|
|
21
|
+
* view pick` / `crtr view run <name>`).
|
|
17
22
|
* All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
|
|
18
23
|
* reads the raw user text. */
|
|
19
24
|
export declare const CANVAS_EXTENSIONS: string[];
|
|
@@ -57,3 +62,41 @@ export declare function buildPiArgv(meta: NodeMeta, opts?: {
|
|
|
57
62
|
resumeSessionPath?: string;
|
|
58
63
|
forkFrom?: string;
|
|
59
64
|
}): PiInvocation;
|
|
65
|
+
/** The pi-SDK launch config the headless broker drives an in-process engine
|
|
66
|
+
* with — the structural inverse of `buildPiArgv`'s flag vocabulary. Each field
|
|
67
|
+
* maps one of buildPiArgv's emitted flags back to its SDK option:
|
|
68
|
+
* `-e`→`extensionPaths`, `-n`→`editorName`, `--fork`→`forkFrom`,
|
|
69
|
+
* `--session`→`resumeSessionPath|resumeSessionId`, `--model`→`model`,
|
|
70
|
+
* `--tools`→`tools`, `--append-system-prompt`→`appendSystemPromptPath`, the
|
|
71
|
+
* trailing positional→`firstPrompt`. */
|
|
72
|
+
export interface BrokerSdkConfig {
|
|
73
|
+
/** The node's pinned working dir (from `CRTR_NODE_CWD`, else `process.cwd()`). */
|
|
74
|
+
cwd: string;
|
|
75
|
+
/** Absolute `.js` extension paths (the canvas extensions). */
|
|
76
|
+
extensionPaths: string[];
|
|
77
|
+
/** Session label (`-n`). */
|
|
78
|
+
editorName?: string;
|
|
79
|
+
/** Spawn-time fork source (`--fork <path|id>`). */
|
|
80
|
+
forkFrom?: string;
|
|
81
|
+
/** Resume by absolute `.jsonl` path (preferred — `SessionManager.open`). */
|
|
82
|
+
resumeSessionPath?: string;
|
|
83
|
+
/** Resume by bare session uuid (legacy fallback). */
|
|
84
|
+
resumeSessionId?: string;
|
|
85
|
+
/** Model spec, e.g. `anthropic/sonnet` (`--model`). */
|
|
86
|
+
model?: string;
|
|
87
|
+
/** Tool allowlist (`--tools a,b,c` → `['a','b','c']`). */
|
|
88
|
+
tools?: string[];
|
|
89
|
+
/** `--append-system-prompt` arg — a file path in practice (pi's loader
|
|
90
|
+
* resolves a path or literal text identically). */
|
|
91
|
+
appendSystemPromptPath?: string;
|
|
92
|
+
/** The fresh-start kickoff message (the trailing positional). */
|
|
93
|
+
firstPrompt?: string;
|
|
94
|
+
}
|
|
95
|
+
/** Translate a `PiInvocation` (the recipe `buildPiArgv` produced) into the SDK
|
|
96
|
+
* config the broker hosts an engine with. This is the EXACT inverse of
|
|
97
|
+
* `buildPiArgv` and must track its flag set 1:1 — co-located here so the two
|
|
98
|
+
* never drift. Safe because `buildPiArgv` is the SOLE producer of these flags
|
|
99
|
+
* (we own both ends). SIDE EFFECT: merges `inv.env` into `process.env` so the
|
|
100
|
+
* in-process engine + the bound canvas extensions see the same env a forked pi
|
|
101
|
+
* would (CRTR_NODE_ID, CRTR_ROOT_SESSION, CRTR_SUBTREE, …). */
|
|
102
|
+
export declare function piInvocationToSdkConfig(inv: PiInvocation): BrokerSdkConfig;
|
|
@@ -32,15 +32,20 @@ export const CANVAS_NAV_PATH = resolveExtension('canvas-nav');
|
|
|
32
32
|
export const CANVAS_GOAL_CAPTURE_PATH = resolveExtension('canvas-goal-capture');
|
|
33
33
|
export const CANVAS_PASSIVE_CONTEXT_PATH = resolveExtension('canvas-passive-context');
|
|
34
34
|
export const CANVAS_CONTEXT_INTRO_PATH = resolveExtension('canvas-context-intro');
|
|
35
|
+
export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate');
|
|
35
36
|
export const CANVAS_COMMANDS_PATH = resolveExtension('canvas-commands');
|
|
36
37
|
export const CANVAS_RESUME_PATH = resolveExtension('canvas-resume');
|
|
38
|
+
export const CANVAS_VIEW_PATH = resolveExtension('canvas-view');
|
|
37
39
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
38
40
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
39
41
|
* graph chrome), goal-capture (persist the first user message as the goal),
|
|
40
42
|
* passive-context (drain passive backlog as pre-text on the next message),
|
|
41
43
|
* context-intro (inject the <crtr-context> bearings block as its own session
|
|
42
|
-
* message, once per brand-new chat),
|
|
43
|
-
*
|
|
44
|
+
* message, once per brand-new chat), doc-substrate (the unified document
|
|
45
|
+
* substrate's two hooks: ## Skills + ## Preferences at boot, on-read context
|
|
46
|
+
* injection), commands (the /promote slash-command), resume (the /resume-node
|
|
47
|
+
* whole-canvas picker → `crtr node focus`), view (the /view popup → `crtr
|
|
48
|
+
* view pick` / `crtr view run <name>`).
|
|
44
49
|
* All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
|
|
45
50
|
* reads the raw user text. */
|
|
46
51
|
export const CANVAS_EXTENSIONS = [
|
|
@@ -50,8 +55,10 @@ export const CANVAS_EXTENSIONS = [
|
|
|
50
55
|
CANVAS_GOAL_CAPTURE_PATH,
|
|
51
56
|
CANVAS_PASSIVE_CONTEXT_PATH,
|
|
52
57
|
CANVAS_CONTEXT_INTRO_PATH,
|
|
58
|
+
CANVAS_DOC_SUBSTRATE_PATH,
|
|
53
59
|
CANVAS_COMMANDS_PATH,
|
|
54
60
|
CANVAS_RESUME_PATH,
|
|
61
|
+
CANVAS_VIEW_PATH,
|
|
55
62
|
];
|
|
56
63
|
/** Bare model aliases resolve to the anthropic provider under pi (avoids the
|
|
57
64
|
* bedrock default). Anything with a `/` or an unknown name passes through. */
|
|
@@ -158,3 +165,59 @@ export function buildPiArgv(meta, opts = {}) {
|
|
|
158
165
|
argv.push(opts.prompt);
|
|
159
166
|
return { argv, env: nodeEnv(meta) };
|
|
160
167
|
}
|
|
168
|
+
/** Translate a `PiInvocation` (the recipe `buildPiArgv` produced) into the SDK
|
|
169
|
+
* config the broker hosts an engine with. This is the EXACT inverse of
|
|
170
|
+
* `buildPiArgv` and must track its flag set 1:1 — co-located here so the two
|
|
171
|
+
* never drift. Safe because `buildPiArgv` is the SOLE producer of these flags
|
|
172
|
+
* (we own both ends). SIDE EFFECT: merges `inv.env` into `process.env` so the
|
|
173
|
+
* in-process engine + the bound canvas extensions see the same env a forked pi
|
|
174
|
+
* would (CRTR_NODE_ID, CRTR_ROOT_SESSION, CRTR_SUBTREE, …). */
|
|
175
|
+
export function piInvocationToSdkConfig(inv) {
|
|
176
|
+
for (const [k, v] of Object.entries(inv.env))
|
|
177
|
+
process.env[k] = v;
|
|
178
|
+
const cfg = {
|
|
179
|
+
cwd: inv.env['CRTR_NODE_CWD'] ?? process.cwd(),
|
|
180
|
+
extensionPaths: [],
|
|
181
|
+
};
|
|
182
|
+
const argv = inv.argv;
|
|
183
|
+
for (let i = 0; i < argv.length; i++) {
|
|
184
|
+
const tok = argv[i];
|
|
185
|
+
switch (tok) {
|
|
186
|
+
case '-e':
|
|
187
|
+
cfg.extensionPaths.push(argv[++i]);
|
|
188
|
+
break;
|
|
189
|
+
case '-n':
|
|
190
|
+
cfg.editorName = argv[++i];
|
|
191
|
+
break;
|
|
192
|
+
case '--fork':
|
|
193
|
+
cfg.forkFrom = argv[++i];
|
|
194
|
+
break;
|
|
195
|
+
case '--session': {
|
|
196
|
+
const v = argv[++i];
|
|
197
|
+
// A path (contains `/` or ends `.jsonl`) resumes via SessionManager.open;
|
|
198
|
+
// a bare uuid is the legacy fallback. Same classification buildPiArgv
|
|
199
|
+
// documents (path preferred, immune to cwd discrepancy).
|
|
200
|
+
if (v.includes('/') || v.endsWith('.jsonl'))
|
|
201
|
+
cfg.resumeSessionPath = v;
|
|
202
|
+
else
|
|
203
|
+
cfg.resumeSessionId = v;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case '--model':
|
|
207
|
+
cfg.model = argv[++i];
|
|
208
|
+
break;
|
|
209
|
+
case '--tools':
|
|
210
|
+
cfg.tools = argv[++i].split(',').filter((t) => t !== '');
|
|
211
|
+
break;
|
|
212
|
+
case '--append-system-prompt':
|
|
213
|
+
cfg.appendSystemPromptPath = argv[++i];
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
// buildPiArgv consumes every flag's value via the cases above, so any
|
|
217
|
+
// token reaching here is the single trailing positional (the prompt).
|
|
218
|
+
cfg.firstPrompt = tok;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return cfg;
|
|
223
|
+
}
|