@crouton-kit/crouter 0.3.18 → 0.3.20
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/PERSONA.md +1 -0
- package/dist/builtin-personas/design/orchestrator.md +2 -1
- package/dist/builtin-personas/developer/PERSONA.md +1 -0
- package/dist/builtin-personas/developer/orchestrator.md +2 -1
- package/dist/builtin-personas/explore/PERSONA.md +1 -0
- package/dist/builtin-personas/explore/orchestrator.md +4 -0
- package/dist/builtin-personas/general/PERSONA.md +1 -0
- package/dist/builtin-personas/general/orchestrator.md +4 -0
- 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/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/orchestrator.md +2 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
- package/dist/builtin-personas/review/PERSONA.md +1 -0
- package/dist/builtin-personas/review/orchestrator.md +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/PERSONA.md +1 -0
- package/dist/builtin-personas/spec/orchestrator.md +2 -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 +54 -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 +610 -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-attach-limits.test.d.ts +1 -0
- package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
- package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
- package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
- package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
- package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
- package/dist/core/__tests__/broker-dialogs.test.js +126 -0
- package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
- package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +87 -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__/canvas-inbox-watcher-hold.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
- 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/broker-clients.d.ts +43 -0
- package/dist/core/__tests__/helpers/broker-clients.js +178 -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-verbs.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
- package/dist/core/__tests__/live-mutation.test.js +6 -132
- 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 +16 -23
- 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 +91 -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 +54 -4
- package/dist/core/runtime/launch.js +85 -5
- 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 +37 -0
- package/dist/core/runtime/nodes.js +60 -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 +4 -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 +11 -8
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +23 -0
- package/dist/core/runtime/spawn.js +77 -23
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// recycle.ts — the "finalize + reboot this pane" action behind `crtr node recycle`.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Recycle FINALIZES the agent occupying a tmux pane and recycles that pane for
|
|
4
4
|
// fresh work, in three steps:
|
|
5
5
|
//
|
|
6
6
|
// 1. Finalize — push the agent's last surfaced message as a `final` report so
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
// 2. Close — kill the agent's pi (respawn-pane -k tears it down in place).
|
|
9
9
|
// 3. Recycle — boot a fresh resident root in that same pane (a new `crtr`).
|
|
10
10
|
//
|
|
11
|
+
// NOT to be confused with `node demote` (flip-to-terminal IN PLACE, which keeps
|
|
12
|
+
// the agent focused and running): recycle ENDS this agent and boots a brand-new
|
|
13
|
+
// resident root here.
|
|
14
|
+
//
|
|
11
15
|
// The agent's real conversation lives inside pi (not on disk), so the final
|
|
12
16
|
// body is its newest report (which, on a natural stop, IS its last assistant
|
|
13
17
|
// message) — falling back to a short note when it never reported.
|
|
@@ -16,10 +20,11 @@ import { join } from 'node:path';
|
|
|
16
20
|
import { getNode, setPresence, updateNode, setFocusOccupant, fullName } from '../canvas/index.js';
|
|
17
21
|
import { reportsDir } from '../canvas/paths.js';
|
|
18
22
|
import { pushFinal } from '../feed/feed.js';
|
|
19
|
-
import { spawnNode, nodeSession } from './nodes.js';
|
|
23
|
+
import { spawnNode, nodeSession, rootOfSpine } from './nodes.js';
|
|
20
24
|
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
21
25
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
22
26
|
import { focusOf, recycleFocusPane, piCommand, paneLocation } from './placement.js';
|
|
27
|
+
import { hostFor } from './host.js';
|
|
23
28
|
import { ensureDaemon } from '../../daemon/manage.js';
|
|
24
29
|
/** The agent's most recent surfaced message: the newest reports/*.md body with
|
|
25
30
|
* its YAML frontmatter stripped. Empty string when the node never reported. */
|
|
@@ -49,18 +54,18 @@ function lastReportBody(nodeId) {
|
|
|
49
54
|
}
|
|
50
55
|
/** Finish `nodeId` and recycle its pane into a fresh root. `callerPane` is the
|
|
51
56
|
* tmux pane the agent occupies (the Alt+C menu passes it as `#{pane_id}`).
|
|
52
|
-
* Best-effort; `
|
|
53
|
-
export async function
|
|
57
|
+
* Best-effort; `recycled:false` when there is no pane to act on. */
|
|
58
|
+
export async function recycleNode(nodeId, callerPane) {
|
|
54
59
|
const meta = getNode(nodeId);
|
|
55
60
|
if (meta === null)
|
|
56
|
-
return {
|
|
61
|
+
return { recycled: false, finalized: false, newRoot: null, delivered: [] };
|
|
57
62
|
const pane = callerPane ?? process.env['TMUX_PANE'];
|
|
58
63
|
if (pane === undefined || pane === '') {
|
|
59
|
-
return {
|
|
64
|
+
return { recycled: false, finalized: false, newRoot: null, delivered: [] };
|
|
60
65
|
}
|
|
61
66
|
// 1. Finalize — fan the agent's last message out as a `final`, mark it done.
|
|
62
67
|
const body = lastReportBody(nodeId) ||
|
|
63
|
-
`Closed via
|
|
68
|
+
`Closed via recycle — no final summary was authored by ${meta.name}.`;
|
|
64
69
|
let delivered = [];
|
|
65
70
|
let finalized = false;
|
|
66
71
|
try {
|
|
@@ -69,6 +74,17 @@ export async function demoteNode(nodeId, callerPane) {
|
|
|
69
74
|
finalized = true;
|
|
70
75
|
}
|
|
71
76
|
catch { /* recycle the pane even if the report failed */ }
|
|
77
|
+
// A broker node has NO tmux pane, so recycleFocusPane below (respawn-pane -k)
|
|
78
|
+
// never kills its engine — route its teardown through the Host seam so the
|
|
79
|
+
// broker PROCESS exits and releases the sole .jsonl writer (mirrors the T12
|
|
80
|
+
// close.ts/reset.ts fix; review reuse MINOR-3). Status is already flipped done
|
|
81
|
+
// by pushFinal above (crash-safe order: the daemon won't revive a done node).
|
|
82
|
+
if (meta.host_kind === 'broker') {
|
|
83
|
+
try {
|
|
84
|
+
hostFor(meta).teardown(nodeId);
|
|
85
|
+
}
|
|
86
|
+
catch { /* best-effort */ }
|
|
87
|
+
}
|
|
72
88
|
// Capture M's focus viewport (if any) BEFORE nulling — the fresh root inherits
|
|
73
89
|
// it (the SAME focus row + pane). The demoted node no longer holds a pane: it is
|
|
74
90
|
// being reclaimed.
|
|
@@ -93,7 +109,7 @@ export async function demoteNode(nodeId, callerPane) {
|
|
|
93
109
|
parent: null,
|
|
94
110
|
launch,
|
|
95
111
|
});
|
|
96
|
-
// REVIVE-HOME: a
|
|
112
|
+
// REVIVE-HOME: a recycled root's durable revive target is the session
|
|
97
113
|
// of the pane it was recycled into (the one place home_session is rewritten
|
|
98
114
|
// after birth). Falls back to the backstage when the pane can't be located.
|
|
99
115
|
updateNode(root.node_id, { home_session: loc?.session ?? nodeSession() });
|
|
@@ -107,9 +123,9 @@ export async function demoteNode(nodeId, callerPane) {
|
|
|
107
123
|
}
|
|
108
124
|
const fresh = getNode(root.node_id);
|
|
109
125
|
const inv = buildPiArgv(fresh);
|
|
110
|
-
const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), [FRONT_DOOR_ENV]: '1' };
|
|
126
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), CRTR_SUBTREE: rootOfSpine(root.node_id), [FRONT_DOOR_ENV]: '1' };
|
|
111
127
|
const ok = recycleFocusPane(root.node_id, pane, {
|
|
112
128
|
command: piCommand(inv.argv), env, cwd: meta.cwd, name: fullName(fresh), resuming: false,
|
|
113
129
|
});
|
|
114
|
-
return {
|
|
130
|
+
return { recycled: ok, finalized, newRoot: root.node_id, delivered };
|
|
115
131
|
}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
import { existsSync, rmSync } from 'node:fs';
|
|
23
23
|
import { getNode, updateNode, setPresence, clearPid, setFocusOccupant, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, nodeDir, openDb, } from '../canvas/index.js';
|
|
24
24
|
import { transition } from './lifecycle.js';
|
|
25
|
-
import {
|
|
25
|
+
import { hostFor } from './host.js';
|
|
26
|
+
import { paneLocation, focusOf } from './placement.js';
|
|
26
27
|
import { buildLaunchSpec } from './launch.js';
|
|
27
28
|
import { roadmapPath } from './roadmap.js';
|
|
28
29
|
import { spawnNode, newNodeId, nodeSession } from './nodes.js';
|
|
@@ -49,11 +50,12 @@ export function reapDescendants(rootId) {
|
|
|
49
50
|
try {
|
|
50
51
|
// Reap BEFORE tearing down the placement (the crash-safety invariant the
|
|
51
52
|
// `cancel` event encodes): a non-supervised status + cleared intent first, so
|
|
52
|
-
// the daemon can't revive a descendant mid-teardown.
|
|
53
|
-
// closes any focus row it held,
|
|
54
|
-
// LOCATION
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// the daemon can't revive a descendant mid-teardown. Teardown then routes
|
|
54
|
+
// through the Host seam (hostFor): a tmux node closes any focus row it held,
|
|
55
|
+
// kills its pane (pane-keyed), and nulls its LOCATION; a broker node sends
|
|
56
|
+
// the `shutdown` frame so its PROCESS exits and releases the .jsonl writer.
|
|
57
|
+
const m = transition(id, 'cancel');
|
|
58
|
+
hostFor(m).teardown(id);
|
|
57
59
|
reaped.push(id);
|
|
58
60
|
}
|
|
59
61
|
catch {
|
|
@@ -110,7 +112,7 @@ export function resetRoot(nodeId, newSessionId, newSessionFile) {
|
|
|
110
112
|
// Re-seed persona_ack to the fresh persona so the pristine `/new`
|
|
111
113
|
// conversation never gets a spurious mode/lifecycle transition steer (the
|
|
112
114
|
// persona injector compares against this ack).
|
|
113
|
-
const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
115
|
+
const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false, model: meta.model_override ?? undefined });
|
|
114
116
|
updateNode(nodeId, {
|
|
115
117
|
mode: 'base',
|
|
116
118
|
lifecycle: 'resident',
|
|
@@ -178,7 +180,7 @@ export function relaunchRoot(oldId, pane, deps = {}) {
|
|
|
178
180
|
window: oldMeta.window ?? null,
|
|
179
181
|
};
|
|
180
182
|
const newId = newNodeId();
|
|
181
|
-
const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
183
|
+
const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false, model: oldMeta.model_override ?? undefined });
|
|
182
184
|
// Park-old + mint-new is the single most fragile spot in the runtime, so it is
|
|
183
185
|
// ONE atomic unit: every ROW write below runs inside a sqlite transaction. A
|
|
184
186
|
// failure anywhere — including the respawn DISPATCH — rolls the whole thing
|
|
@@ -204,6 +206,7 @@ export function relaunchRoot(oldId, pane, deps = {}) {
|
|
|
204
206
|
parent: null,
|
|
205
207
|
spawnedBy: oldId, // audit-only successor link; does NOT touch the spine
|
|
206
208
|
nodeId: newId,
|
|
209
|
+
modelOverride: oldMeta.model_override,
|
|
207
210
|
launch,
|
|
208
211
|
});
|
|
209
212
|
transition(newId, 'yield'); // active (from spawn) + intent=refresh safety net
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type NodeMeta } from '../canvas/index.js';
|
|
2
|
+
import type { WakeOrigin } from './bearings.js';
|
|
2
3
|
import { type RespawnPaneOpts } from './placement.js';
|
|
3
4
|
/** Pick the `--session` source for a revive. resume=true prefers the absolute
|
|
4
5
|
* session-file path (immune to cwd; pi opens it directly) and keeps the bare
|
|
@@ -25,6 +26,7 @@ export interface ReviveResult {
|
|
|
25
26
|
*/
|
|
26
27
|
export declare function reviveNode(nodeId: string, opts: {
|
|
27
28
|
resume: boolean;
|
|
29
|
+
wakeReason?: WakeOrigin;
|
|
28
30
|
}): ReviveResult;
|
|
29
31
|
/** Re-exec a node's pi FRESH in its EXISTING tmux pane (the refresh-yield
|
|
30
32
|
* path). Unlike `reviveNode`, this opens no new window: the pane's current pi
|
|
@@ -14,26 +14,15 @@
|
|
|
14
14
|
// preferring the absolute session-file path (cwd-immune) over
|
|
15
15
|
// the bare session id.
|
|
16
16
|
// resume=false → fresh pi invocation — the node re-reads its roadmap/context dir.
|
|
17
|
-
import { getNode, updateNode, setPresence, clearPid, fullName, } from '../canvas/index.js';
|
|
17
|
+
import { getNode, updateNode, setPresence, clearPid, fullName, cancelDeadlinesFor, } from '../canvas/index.js';
|
|
18
18
|
import { transition } from './lifecycle.js';
|
|
19
19
|
import { buildPiArgv } from './launch.js';
|
|
20
20
|
import { buildReviveKickoff, drainBearings } from './kickoff.js';
|
|
21
21
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function pidAlive(pid) {
|
|
27
|
-
if (pid == null)
|
|
28
|
-
return false;
|
|
29
|
-
try {
|
|
30
|
-
process.kill(pid, 0);
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
return e.code === 'EPERM';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
22
|
+
import { reconcile, piCommand, respawnPane, } from './placement.js';
|
|
23
|
+
import { hostFor } from './host.js';
|
|
24
|
+
import { nodeSession, childBackstageOf, rootOfSpine } from './nodes.js';
|
|
25
|
+
import { isPidAlive } from '../canvas/pid.js';
|
|
37
26
|
// ---------------------------------------------------------------------------
|
|
38
27
|
// resumeArgs — which session source a revive resumes from
|
|
39
28
|
// ---------------------------------------------------------------------------
|
|
@@ -73,7 +62,7 @@ export function reviveNode(nodeId, opts) {
|
|
|
73
62
|
// frozen pane), so the guard gates on pi liveness too, not pane-existence alone.
|
|
74
63
|
reconcile(nodeId);
|
|
75
64
|
const live = getNode(nodeId) ?? meta;
|
|
76
|
-
if (
|
|
65
|
+
if (hostFor(live).isAlive(nodeId) && isPidAlive(live.pi_pid)) {
|
|
77
66
|
return {
|
|
78
67
|
window: live.window ?? null,
|
|
79
68
|
session: live.tmux_session ?? nodeSession(),
|
|
@@ -98,8 +87,12 @@ export function reviveNode(nodeId, opts) {
|
|
|
98
87
|
inv = buildPiArgv(meta, resume);
|
|
99
88
|
}
|
|
100
89
|
else {
|
|
90
|
+
// A fresh revive: drain the one-shot bearings, then build the kickoff. When
|
|
91
|
+
// a scheduled bare self-alarm drove this revive, opts.wakeReason carries the
|
|
92
|
+
// wake provenance so the kickoff leads with a <crtr-wake> block ("a timer
|
|
93
|
+
// woke you"); every other reviveNode caller passes nothing → no block.
|
|
101
94
|
const bearings = drainBearings(meta);
|
|
102
|
-
inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
|
|
95
|
+
inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings, opts.wakeReason) });
|
|
103
96
|
}
|
|
104
97
|
// Placement owns WHERE this revive lands (§1.4): resume into a live focus pane
|
|
105
98
|
// if the node occupies one, else a fresh window in its home_session (the
|
|
@@ -108,9 +101,14 @@ export function reviveNode(nodeId, opts) {
|
|
|
108
101
|
// around it (the crash-safety ordering, unchanged). THIS is the bug-kill: a
|
|
109
102
|
// non-focused background revive can no longer new-window into a user session.
|
|
110
103
|
transition(nodeId, 'revive');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
// Cancel-on-wake (design §6.4, AC-E1): every revive-for-any-reason (an inbox
|
|
105
|
+
// event, a different wake, a manual focus) drops this node's pending deadline,
|
|
106
|
+
// so the deadline always belongs to the dormancy being left. Writes only the
|
|
107
|
+
// wakeups table, after the atomic transition above. (reviveInPlace's
|
|
108
|
+
// transition('boot') needs NO hook — a refresh is the same node continuing,
|
|
109
|
+
// not leaving a dormancy.)
|
|
110
|
+
cancelDeadlinesFor(nodeId);
|
|
111
|
+
const placed = hostFor(meta).launch(nodeId, inv, {
|
|
114
112
|
cwd: meta.cwd,
|
|
115
113
|
name: fullName(meta),
|
|
116
114
|
resuming,
|
|
@@ -150,14 +148,16 @@ export function reviveInPlace(nodeId, pane, respawn = respawnPane) {
|
|
|
150
148
|
// then build purely.
|
|
151
149
|
const bearings = drainBearings(meta);
|
|
152
150
|
const inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
|
|
153
|
-
// CRTR_ROOT_SESSION is the backstage this node's CHILDREN spawn into —
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// session
|
|
160
|
-
|
|
151
|
+
// CRTR_ROOT_SESSION is the backstage this node's CHILDREN spawn into — NOT the
|
|
152
|
+
// pane's live `session`. A FOCUSED child's pane is in a USER session (focus
|
|
153
|
+
// taints meta.tmux_session), so sourcing it from `session` would land any
|
|
154
|
+
// child it spawns in the user's session, re-tainting that child's
|
|
155
|
+
// home_session (A-MAJOR-1). childBackstageOf is the taint-immune backstage:
|
|
156
|
+
// home_session for a managed child, but `nodeSession()` for a ROOT (whose
|
|
157
|
+
// home_session may itself be a user session it adopted via the inline front
|
|
158
|
+
// door — sourcing children from it would re-point the root's whole subtree
|
|
159
|
+
// into the user's session on every refresh-yield).
|
|
160
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: childBackstageOf(nodeId), CRTR_SUBTREE: rootOfSpine(nodeId) };
|
|
161
161
|
const ok = respawn({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
|
|
162
162
|
if (!ok) {
|
|
163
163
|
throw new Error(`reviveInPlace: respawn-pane dispatch failed for ${nodeId}`);
|
|
@@ -199,11 +199,11 @@ export function relaunchRootInPane(nodeId, pane) {
|
|
|
199
199
|
}
|
|
200
200
|
// No prompt, no resume → a brand-new root conversation at cycle 0.
|
|
201
201
|
const inv = buildPiArgv(meta, {});
|
|
202
|
-
// Source CRTR_ROOT_SESSION from the
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// —
|
|
206
|
-
const env = { ...inv.env, CRTR_ROOT_SESSION:
|
|
202
|
+
// Source CRTR_ROOT_SESSION from childBackstageOf, the same backstage rule as
|
|
203
|
+
// reviveInPlace. relaunchRootInPane runs only on a root, whose children must
|
|
204
|
+
// flow to the shared backstage `nodeSession()` — never the root's own adopted
|
|
205
|
+
// user session — so both in-pane revive paths stay sourced identically.
|
|
206
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: childBackstageOf(nodeId), CRTR_SUBTREE: rootOfSpine(nodeId), [FRONT_DOOR_ENV]: '1' };
|
|
207
207
|
const ok = respawnPane({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
|
|
208
208
|
if (!ok) {
|
|
209
209
|
throw new Error(`relaunchRootInPane: respawn-pane dispatch failed for ${nodeId}`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WakeOrigin } from './bearings.js';
|
|
1
2
|
import { type NodeMeta, type Mode } from '../canvas/index.js';
|
|
2
3
|
export interface BootRootOpts {
|
|
3
4
|
cwd: string;
|
|
@@ -27,6 +28,21 @@ export interface SpawnChildOpts {
|
|
|
27
28
|
* new session for the child — the source is untouched — then `prompt` is the
|
|
28
29
|
* next message. A one-shot at birth; the child resumes its own session after. */
|
|
29
30
|
forkFrom?: string;
|
|
31
|
+
/** Set ONLY by the daemon when a `spawn`/spawn-cron wake births this node: the
|
|
32
|
+
* provenance of the timer that fired (see WakeOrigin). In-memory only — it is
|
|
33
|
+
* NOT part of the stored recipe (the daemon spreads it in at fire time via
|
|
34
|
+
* `spawnChild({ ...recipe, wakeOrigin })`); `node new` never sets it. When set,
|
|
35
|
+
* a <crtr-wake> block is prepended to the kickoff so the newborn knows a clock
|
|
36
|
+
* birthed it. */
|
|
37
|
+
wakeOrigin?: WakeOrigin;
|
|
38
|
+
/** Which HOST launches + supervises this node: a tmux pane (default) or the
|
|
39
|
+
* headless broker. Persisted as `host_kind` at birth (resolved from
|
|
40
|
+
* `--headless` / the `headless` config default by the caller). */
|
|
41
|
+
hostKind?: 'tmux' | 'broker';
|
|
42
|
+
/** Pin the node to a model TIER (ultra/strong/medium/light), overriding the
|
|
43
|
+
* persona's declared default. Persisted to `meta.model_override` so it
|
|
44
|
+
* survives polymorphs. Omit to use the persona default. */
|
|
45
|
+
model?: string;
|
|
30
46
|
}
|
|
31
47
|
/** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
|
|
32
48
|
* A live node id resolves to its captured session FILE (absolute, cwd-immune),
|
|
@@ -38,6 +54,13 @@ export interface SpawnChildResult {
|
|
|
38
54
|
window: string | null;
|
|
39
55
|
session: string;
|
|
40
56
|
}
|
|
57
|
+
/** Resolve who a spawn is attributed to. A managed child needs a spine parent
|
|
58
|
+
* (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
|
|
59
|
+
* does not: it is top-level by definition and the spawner identity is
|
|
60
|
+
* provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
|
|
61
|
+
* spawner (regression: `crtr node new --root` from outside a node used to
|
|
62
|
+
* throw here). */
|
|
63
|
+
export declare function resolveSpawner(parent: string | undefined, ctxNodeId: string | null, root: boolean): string | null;
|
|
41
64
|
/** Spawn a node from a live node. By default a managed terminal worker in a
|
|
42
65
|
* background window, with the spawner auto-subscribed (active) via spawnNode.
|
|
43
66
|
* With `root`: an independent resident root — parent=null, NO subscription back
|
|
@@ -10,16 +10,17 @@
|
|
|
10
10
|
// provenance via spawned_by) brought forefront for direct driving.
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
12
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
13
|
-
import { spawnNode, currentNodeContext, resolveBirthSession, nodeSession } from './nodes.js';
|
|
13
|
+
import { spawnNode, currentNodeContext, resolveBirthSession, nodeSession, rootOfSpine } from './nodes.js';
|
|
14
14
|
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
15
15
|
import { writeGoal } from './kickoff.js';
|
|
16
16
|
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
17
|
-
import { seedMemory, seedUserMemory, seedProjectMemory } from './memory.js';
|
|
18
17
|
import { generateSessionName } from './naming.js';
|
|
19
|
-
import {
|
|
18
|
+
import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
|
|
19
|
+
import { installMenuBinding, installNavBindings, installViewNavBindings } from './tmux-chrome.js';
|
|
20
20
|
import { setPresence, updateNode, getNode, fullName } from '../canvas/index.js';
|
|
21
21
|
import { registerRootFocus, ensureSession, openNodeWindow, piCommand, currentTmux, inTmux, focusWindow, } from './placement.js';
|
|
22
22
|
import { transition } from './lifecycle.js';
|
|
23
|
+
import { headlessBrokerHost } from './host.js';
|
|
23
24
|
import { ensureDaemon } from '../../daemon/manage.js';
|
|
24
25
|
/** Create a root node and bring up its pi. Returns the node; for 'inline' this
|
|
25
26
|
* only returns after pi exits (it took over the terminal). */
|
|
@@ -57,8 +58,8 @@ export function bootRoot(opts) {
|
|
|
57
58
|
// Every node window — root or child — lives in the one shared session.
|
|
58
59
|
const session = nodeSession();
|
|
59
60
|
ensureSession(session, opts.cwd);
|
|
60
|
-
// Make the Alt+C action menu + Alt+] / Alt+[ nav keys
|
|
61
|
-
// (idempotent, in-tmux only).
|
|
61
|
+
// Make the Alt+C action menu + Alt+] / Alt+[ node-nav keys + Alt+V then ]/[
|
|
62
|
+
// view-nav chord live on this server (idempotent, in-tmux only).
|
|
62
63
|
if (inTmux()) {
|
|
63
64
|
try {
|
|
64
65
|
installMenuBinding();
|
|
@@ -68,6 +69,10 @@ export function bootRoot(opts) {
|
|
|
68
69
|
installNavBindings();
|
|
69
70
|
}
|
|
70
71
|
catch { /* best-effort */ }
|
|
72
|
+
try {
|
|
73
|
+
installViewNavBindings();
|
|
74
|
+
}
|
|
75
|
+
catch { /* best-effort */ }
|
|
71
76
|
}
|
|
72
77
|
// inline: the root's pi takes over THIS terminal, so its own window stays
|
|
73
78
|
// where the user is (its tmux_session tracks that real pane so supervision
|
|
@@ -93,7 +98,7 @@ export function bootRoot(opts) {
|
|
|
93
98
|
}
|
|
94
99
|
const withSession = getNode(meta.node_id);
|
|
95
100
|
const inv = buildPiArgv(withSession, { prompt: opts.prompt });
|
|
96
|
-
const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: session, [FRONT_DOOR_ENV]: '1' };
|
|
101
|
+
const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: session, CRTR_SUBTREE: rootOfSpine(meta.node_id), [FRONT_DOOR_ENV]: '1' };
|
|
97
102
|
const r = spawnSync('pi', inv.argv, { stdio: 'inherit', env });
|
|
98
103
|
process.exit(r.status ?? 0);
|
|
99
104
|
}
|
|
@@ -120,6 +125,19 @@ export function resolveForkSource(value) {
|
|
|
120
125
|
// Not a known node — treat as a bare/partial pi session id for pi to resolve.
|
|
121
126
|
return v;
|
|
122
127
|
}
|
|
128
|
+
/** Resolve who a spawn is attributed to. A managed child needs a spine parent
|
|
129
|
+
* (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
|
|
130
|
+
* does not: it is top-level by definition and the spawner identity is
|
|
131
|
+
* provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
|
|
132
|
+
* spawner (regression: `crtr node new --root` from outside a node used to
|
|
133
|
+
* throw here). */
|
|
134
|
+
export function resolveSpawner(parent, ctxNodeId, root) {
|
|
135
|
+
const spawner = parent ?? ctxNodeId ?? null;
|
|
136
|
+
if (!root && spawner === null) {
|
|
137
|
+
throw new Error('spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent');
|
|
138
|
+
}
|
|
139
|
+
return spawner;
|
|
140
|
+
}
|
|
123
141
|
/** Spawn a node from a live node. By default a managed terminal worker in a
|
|
124
142
|
* background window, with the spawner auto-subscribed (active) via spawnNode.
|
|
125
143
|
* With `root`: an independent resident root — parent=null, NO subscription back
|
|
@@ -131,11 +149,8 @@ export function spawnChild(opts) {
|
|
|
131
149
|
}
|
|
132
150
|
catch { /* daemon is best-effort */ }
|
|
133
151
|
const ctx = currentNodeContext();
|
|
134
|
-
const spawner = opts.parent ?? ctx.nodeId;
|
|
135
|
-
if (spawner === null || spawner === undefined) {
|
|
136
|
-
throw new Error('spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent');
|
|
137
|
-
}
|
|
138
152
|
const root = opts.root === true;
|
|
153
|
+
const spawner = resolveSpawner(opts.parent, ctx.nodeId ?? null, root);
|
|
139
154
|
const mode = opts.mode ?? 'base';
|
|
140
155
|
// Lifecycle keys on ROOT-ness only, independent of mode: an independent root
|
|
141
156
|
// (or `--root`) is resident (a conversation that persists, woken by inbox/
|
|
@@ -146,7 +161,7 @@ export function spawnChild(opts) {
|
|
|
146
161
|
// Spine: a managed child reports up to its spawner (has a manager); an
|
|
147
162
|
// independent root sits top-of-spine with nobody to push to. Mirrors the
|
|
148
163
|
// `parent` set below (root ? null : spawner), so hasManager === parent!==null.
|
|
149
|
-
const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root });
|
|
164
|
+
const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root, model: opts.model });
|
|
150
165
|
// Name the worker from its task now, so its first editor label carries it.
|
|
151
166
|
const meta = spawnNode({
|
|
152
167
|
kind: opts.kind,
|
|
@@ -156,9 +171,16 @@ export function spawnChild(opts) {
|
|
|
156
171
|
name: opts.name ?? opts.kind,
|
|
157
172
|
description: generateSessionName(opts.prompt),
|
|
158
173
|
// A root has no spine parent (top-level, nobody subscribes); it still
|
|
159
|
-
// records spawned_by=spawner
|
|
174
|
+
// records spawned_by=spawner when a node (not a human shell) spawned it.
|
|
175
|
+
// A child's parent IS its manager.
|
|
160
176
|
parent: root ? null : spawner,
|
|
161
|
-
spawnedBy: root ? spawner : undefined,
|
|
177
|
+
spawnedBy: root ? (spawner ?? undefined) : undefined,
|
|
178
|
+
// Persist the RAW fork reference (not the resolved path) as provenance, so
|
|
179
|
+
// the boot intro can detect this is a fork and re-assert the node's own
|
|
180
|
+
// identity over the source's copied-in conversation.
|
|
181
|
+
forkFrom: opts.forkFrom,
|
|
182
|
+
hostKind: opts.hostKind,
|
|
183
|
+
modelOverride: opts.model,
|
|
162
184
|
launch,
|
|
163
185
|
});
|
|
164
186
|
// Persist the task as the child's goal for a fresh revive to re-read.
|
|
@@ -168,6 +190,19 @@ export function spawnChild(opts) {
|
|
|
168
190
|
// — not in buildPiArgv — so a bad reference fails the spawn loudly before any
|
|
169
191
|
// window opens, rather than after pi is already booting.
|
|
170
192
|
const forkFrom = opts.forkFrom !== undefined ? resolveForkSource(opts.forkFrom) : undefined;
|
|
193
|
+
// A fork inherits the SOURCE node's entire first-person conversation, so the
|
|
194
|
+
// identity re-assertion must ride the STRONGEST channel too, not only the
|
|
195
|
+
// session-start bearings (a trailing custom_message the model weighs only by
|
|
196
|
+
// recency). Prepend it to the kickoff PROMPT — the message that actually
|
|
197
|
+
// triggers the fork's first turn, so the model acts on "you are node X, a
|
|
198
|
+
// FORK of <source>, NOT them" as live instruction. When the daemon births this
|
|
199
|
+
// node from a scheduled `spawn` wake, the <crtr-wake> provenance block leads
|
|
200
|
+
// (so "a timer birthed you" precedes the task) — mirroring the fork-identity
|
|
201
|
+
// prepend. Only the pi argv prompt is reframed; the persisted goal (writeGoal
|
|
202
|
+
// above) keeps the raw task.
|
|
203
|
+
const wakeBlock = opts.wakeOrigin !== undefined ? `${buildWakeBearings(opts.wakeOrigin)}\n\n` : '';
|
|
204
|
+
const idBlock = forkFrom !== undefined ? `${buildIdentityAssertion(meta.node_id)}\n\n` : '';
|
|
205
|
+
const kickoff = `${wakeBlock}${idBlock}${opts.prompt}`;
|
|
171
206
|
// A child created DIRECTLY as an orchestrator (mode='orchestrator') boots
|
|
172
207
|
// with the orchestrator persona but bypasses promote(), which is where a
|
|
173
208
|
// roadmap scaffold would normally be seeded. Lay one down here (goal
|
|
@@ -176,14 +211,8 @@ export function spawnChild(opts) {
|
|
|
176
211
|
if (mode === 'orchestrator' && !hasRoadmap(meta.node_id)) {
|
|
177
212
|
seedRoadmap(meta.node_id, { goal: opts.prompt.trim() });
|
|
178
213
|
}
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// (keyed off the child's cwd), and node-local. Each guarded against clobber.
|
|
182
|
-
if (mode === 'orchestrator') {
|
|
183
|
-
seedUserMemory();
|
|
184
|
-
seedProjectMemory(opts.cwd);
|
|
185
|
-
seedMemory(meta.node_id);
|
|
186
|
-
}
|
|
214
|
+
// (The three scoped long-term memory stores are seeded for EVERY node at birth
|
|
215
|
+
// in spawnNode — no orchestrator-gated seeding needed here.)
|
|
187
216
|
// A managed CHILD lands in the shared global session: inherited from the
|
|
188
217
|
// parent's CRTR_ROOT_SESSION, else the default node session. A --root spawned
|
|
189
218
|
// from inside tmux instead opens its window in the CALLER'S CURRENT session,
|
|
@@ -205,8 +234,33 @@ export function spawnChild(opts) {
|
|
|
205
234
|
// backstage, never a user session — this is what keeps a background revive
|
|
206
235
|
// off the user's screen (the focus taint cannot reach it).
|
|
207
236
|
updateNode(meta.node_id, { home_session: session });
|
|
208
|
-
const inv = buildPiArgv(meta, { prompt:
|
|
209
|
-
|
|
237
|
+
const inv = buildPiArgv(meta, { prompt: kickoff, forkFrom });
|
|
238
|
+
// Belt-and-suspenders backstage routing emitted at EVERY launch site: the
|
|
239
|
+
// authoritative CRTR_ROOT_SESSION (this subtree's shared backstage) +
|
|
240
|
+
// CRTR_SUBTREE (this node's spine root). Hoisted ABOVE the host branch so the
|
|
241
|
+
// broker launch and the tmux command consume ONE authoritative env — the
|
|
242
|
+
// broker host merges inv.env, so a --headless --root broker would otherwise
|
|
243
|
+
// inherit the SPAWNER's subtree id via the lossy nodeEnv passthrough (review
|
|
244
|
+
// reuse MINOR-2). FRONT_DOOR is added per-consumer below (the broker host sets
|
|
245
|
+
// it itself; the tmux env const adds it).
|
|
246
|
+
inv.env = { ...inv.env, CRTR_ROOT_SESSION: childSession, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
|
|
247
|
+
// Birth LAUNCH branch on the PERSISTED host_kind (set at birth by spawnNode).
|
|
248
|
+
// A broker birth diverts AWAY from the tmux pane: the headless broker host
|
|
249
|
+
// launches a detached broker process (which records its own pid as pi_pid via
|
|
250
|
+
// the stophook) and returns placement fields all null — so we open NO tmux
|
|
251
|
+
// window and write NO tmux placement (presence stays null for a broker). The
|
|
252
|
+
// tmux path below is left completely UNCHANGED (byte-identical) for every
|
|
253
|
+
// non-broker spawn. Mirrors reviveNode's `hostFor(meta).launch(...)` shape.
|
|
254
|
+
if (meta.host_kind === 'broker') {
|
|
255
|
+
const placed = headlessBrokerHost.launch(meta.node_id, inv, {
|
|
256
|
+
cwd: meta.cwd,
|
|
257
|
+
name: fullName(meta),
|
|
258
|
+
resuming: false,
|
|
259
|
+
});
|
|
260
|
+
const saved = getNode(meta.node_id);
|
|
261
|
+
return { node: saved, window: placed.window, session: placed.session };
|
|
262
|
+
}
|
|
263
|
+
const env = { ...inv.env, [FRONT_DOOR_ENV]: '1' };
|
|
210
264
|
const command = piCommand(inv.argv);
|
|
211
265
|
// openNodeWindow now returns {window, pane}; pane is unused until the
|
|
212
266
|
// placement layer lands, so destructure the window and proceed unchanged.
|
|
@@ -6,7 +6,7 @@ export interface StopSignals {
|
|
|
6
6
|
}
|
|
7
7
|
export type StopAction = {
|
|
8
8
|
action: 'allow';
|
|
9
|
-
reason: 'awaiting' | 'finished' | 'escalated' | 'dormant';
|
|
9
|
+
reason: 'awaiting' | 'scheduled' | 'finished' | 'escalated' | 'dormant';
|
|
10
10
|
} | {
|
|
11
11
|
action: 'reprompt';
|
|
12
12
|
reason: 'stalled';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Also fine.
|
|
17
17
|
// • otherwise → a TERMINAL node with nothing live to wait for and no
|
|
18
18
|
// final pushed. Re-prompt it to finish or escalate.
|
|
19
|
-
import { hasActiveLiveSubscription, getNode } from '../canvas/index.js';
|
|
19
|
+
import { hasActiveLiveSubscription, hasPendingSelfWake, getNode } from '../canvas/index.js';
|
|
20
20
|
export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
|
|
21
21
|
'Run `crtr push final "<result>"` if the work is done, or `crtr human ask` if you are blocked or need the user.';
|
|
22
22
|
/** Decide what to do when a node stops. Pure given the canvas + this turn's
|
|
@@ -35,6 +35,11 @@ export function evaluateStop(nodeId, signals) {
|
|
|
35
35
|
if (node !== null && node.lifecycle === 'resident') {
|
|
36
36
|
return { action: 'allow', reason: 'dormant' };
|
|
37
37
|
}
|
|
38
|
+
// A terminal node with a pending self-scheduled wakeup is legitimately
|
|
39
|
+
// waiting on the clock: the daemon's wakeup sweep resumes it when the wake
|
|
40
|
+
// fires, so it should idle-release rather than busy-wait holding pi/window.
|
|
41
|
+
if (hasPendingSelfWake(nodeId))
|
|
42
|
+
return { action: 'allow', reason: 'scheduled' };
|
|
38
43
|
// A terminal node holding something live to wake it is legitimately awaiting.
|
|
39
44
|
if (hasActiveLiveSubscription(nodeId))
|
|
40
45
|
return { action: 'allow', reason: 'awaiting' };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { installMenuBinding, installNavBindings, sendKeysEnter } from './tmux.js';
|
|
1
|
+
export { installMenuBinding, installNavBindings, installViewNavBindings, sendKeysEnter } from './tmux.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// tmux-chrome.ts — chrome seam (§2.1): stateless keybind/input verbs.
|
|
2
2
|
// The ONLY non-placement module allowed to import the tmux driver, per the
|
|
3
3
|
// §5.1 lint. Re-exports the menu/nav/send-keys verbs callers (spawn, chord) need.
|
|
4
|
-
export { installMenuBinding, installNavBindings, sendKeysEnter } from './tmux.js';
|
|
4
|
+
export { installMenuBinding, installNavBindings, installViewNavBindings, sendKeysEnter } from './tmux.js';
|
|
@@ -96,6 +96,26 @@ export declare function paneLocation(pane: string): {
|
|
|
96
96
|
* liveness. We therefore require the echoed `#{pane_id}` to equal the requested
|
|
97
97
|
* pane: a live pane echoes its own id, a gone/bogus one yields empty. */
|
|
98
98
|
export declare function paneExists(pane: string): boolean;
|
|
99
|
+
/** Every live pane id on the server (`list-panes -a`), as a Set for membership
|
|
100
|
+
* probes. Returns null when tmux is unreachable (no server / transient failure)
|
|
101
|
+
* so callers can tell "no panes" apart from "can't tell" — a GC pass must skip,
|
|
102
|
+
* never mass-delete, on a failed probe. One subprocess call total, so batch
|
|
103
|
+
* liveness sweeps (e.g. the daemon's stale-focus GC) don't pay a per-pane
|
|
104
|
+
* display-message each. */
|
|
105
|
+
export declare function listLivePanes(): Set<string> | null;
|
|
106
|
+
/** The working directory of a pane (`display-message -p -t <pane>
|
|
107
|
+
* '#{pane_current_path}'`). Used to preserve a view monitor's cwd across a
|
|
108
|
+
* view-cycle respawn so project-scoped views still resolve. Null if tmux fails. */
|
|
109
|
+
export declare function paneCurrentPath(pane: string): string | null;
|
|
110
|
+
/** Set a PANE-scoped tmux option (`tmux set-option -p -t <pane> <name> <value>`).
|
|
111
|
+
* Used to tag a pane with the view id it currently hosts (`@crtr_view`) so the
|
|
112
|
+
* view-nav cycle can read it back and switch to the next/prev view in place.
|
|
113
|
+
* Best-effort; never throws. */
|
|
114
|
+
export declare function setPaneOption(pane: string, name: string, value: string): boolean;
|
|
115
|
+
/** Read a PANE-scoped tmux option value (`tmux show-options -p -t <pane> -q -v
|
|
116
|
+
* <name>`): `-v` prints only the value, `-q` suppresses the unknown-option
|
|
117
|
+
* error so an unset option yields an empty string. undefined if tmux fails. */
|
|
118
|
+
export declare function getPaneOption(pane: string, name: string): string | undefined;
|
|
99
119
|
/** Relocate a pane into another session as its own window WITHOUT killing the
|
|
100
120
|
* process in it — `break-pane -d` moves the pane out of its current window (the
|
|
101
121
|
* pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
|
|
@@ -197,3 +217,11 @@ export declare function installMenuBinding(): boolean;
|
|
|
197
217
|
* terminal/tmux disambiguate it from a raw CSI introducer (`extended-keys on`).
|
|
198
218
|
*/
|
|
199
219
|
export declare function installNavBindings(): boolean;
|
|
220
|
+
/** Bind Alt+V → (], [) to the view-monitor cycle. `M-v` enters the private
|
|
221
|
+
* `crtr-view` key table (switch-client -T), then ] cycles next / [ cycles prev.
|
|
222
|
+
* Best-effort; false if any of the three binds fail. Deliberately distinct from
|
|
223
|
+
* installNavBindings' bare Alt+]/Alt+[ (node cycle): the two cycles coexist on
|
|
224
|
+
* the same server — brackets alone walk the node graph, Alt+V-then-bracket flips
|
|
225
|
+
* view monitors. The bracket keys are bound in the private table, so they NEVER
|
|
226
|
+
* shadow the user's root ]/[ and carry no CSI-introducer ambiguity. */
|
|
227
|
+
export declare function installViewNavBindings(): boolean;
|