@crouton-kit/crouter 0.3.17 → 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/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.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/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- 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/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- 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 +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- 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 +660 -65
- 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__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -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__/persona-subkind.test.js +18 -15
- 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/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- 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/types.js +2 -2
- 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/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- 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 +42 -14
- package/dist/core/runtime/placement.js +228 -38
- 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 +25 -1
- package/dist/core/spawn.js +72 -7
- 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 +145 -34
- 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,8 +1,8 @@
|
|
|
1
1
|
import { type NodeRow, type FocusRow } from '../canvas/index.js';
|
|
2
|
-
import { homeSessionOf } from './nodes.js';
|
|
3
|
-
export { homeSessionOf };
|
|
2
|
+
import { homeSessionOf, childBackstageOf } from './nodes.js';
|
|
3
|
+
export { homeSessionOf, childBackstageOf };
|
|
4
4
|
export type { FocusRow };
|
|
5
|
-
export { piCommand, paneLocation, currentTmux, inTmux, ensureSession, openNodeWindow, focusWindow, windowAlive, windowOfPane, respawnPane, } from './tmux.js';
|
|
5
|
+
export { piCommand, paneLocation, currentTmux, inTmux, ensureSession, openNodeWindow, splitWindow, focusWindow, windowAlive, windowOfPane, respawnPane, respawnPaneSync, setPaneOption, getPaneOption, paneCurrentPath, listLivePanes, } from './tmux.js';
|
|
6
6
|
export type { RespawnPaneOpts } from './tmux.js';
|
|
7
7
|
export { nodeSession } from './nodes.js';
|
|
8
8
|
/** The focus a node occupies, or null. UNIQUE(node_id) ⇒ at most one. */
|
|
@@ -15,6 +15,16 @@ export declare function focusByPane(pane: string): FocusRow | null;
|
|
|
15
15
|
export declare function focusedNodes(): Set<string>;
|
|
16
16
|
/** Every focus row (every live viewport). */
|
|
17
17
|
export declare function listFocuses(): FocusRow[];
|
|
18
|
+
/** The on-screen viewport a human-in-the-loop prompt raised by `nodeId` should
|
|
19
|
+
* surface into: the HIGHEST FOCUSED node of nodeId's graph — the focused node
|
|
20
|
+
* closest to the graph root, i.e. the session/window the user is actually
|
|
21
|
+
* watching this work in. Walks nodeId's spine to its root, enumerates the whole
|
|
22
|
+
* tree root-first (`view` is BFS ⇒ shallowest first), and returns the focus row
|
|
23
|
+
* of the first node that occupies a viewport. null when nothing in the graph is
|
|
24
|
+
* on screen — the caller then surfaces in the user's attached pane rather than
|
|
25
|
+
* the backstage node session. PURE (db reads only): no tmux probe, so the pane
|
|
26
|
+
* may be stale; the caller liveness-checks before targeting it. */
|
|
27
|
+
export declare function graphSurfaceTarget(nodeId: string): FocusRow | null;
|
|
18
28
|
/** The cached LOCATION as stored on a node row: the authoritative `pane` handle
|
|
19
29
|
* plus its derived window/session cache. */
|
|
20
30
|
export interface CachedLocation {
|
|
@@ -145,8 +155,10 @@ export declare function reviveTarget(focus: FocusRow | null, focusPaneAlive: boo
|
|
|
145
155
|
* nothing else. That is the structural bug-kill.
|
|
146
156
|
*
|
|
147
157
|
* `setPresence` (the one atomic LOCATION write) records where the node landed.
|
|
148
|
-
* CRTR_ROOT_SESSION is forced to
|
|
149
|
-
* children always flow to the backstage, never
|
|
158
|
+
* CRTR_ROOT_SESSION is forced to the node's children-backstage (childBackstageOf)
|
|
159
|
+
* in BOTH branches so the node's children always flow to the backstage, never
|
|
160
|
+
* into the focus session — and, for a front-door ROOT, never into the user
|
|
161
|
+
* session its own pane adopted. */
|
|
150
162
|
export declare function reviveIntoPlacement(nodeId: string, launch: ReviveLaunch): PlacementResult;
|
|
151
163
|
/** Relocate a node's still-running agent to the background `crtr` session,
|
|
152
164
|
* freeing the foreground pane WITHOUT killing the pi. `break-pane` moves the
|
|
@@ -156,7 +168,15 @@ export declare function reviveIntoPlacement(nodeId: string, launch: ReviveLaunch
|
|
|
156
168
|
* CURRENT location, §2.4) and again after (presence FOLLOWS the move). No-op
|
|
157
169
|
* (false) when there is no live pane to relocate or tmux refuses the break.
|
|
158
170
|
* `pane` is the authoritative node pane the caller acts on (the Alt+C menu's
|
|
159
|
-
* `#{pane_id}`); falls back to the node's durable handle.
|
|
171
|
+
* `#{pane_id}`); falls back to the node's durable handle.
|
|
172
|
+
*
|
|
173
|
+
* Invariant P gate (Bug 1): the backstage holds ONLY generating-but-unfocused
|
|
174
|
+
* nodes. A node that is NOT mid-turn (e.g. a resident root parked idle between
|
|
175
|
+
* turns, detached via Alt+C → D) must never be parked there as a live-idle pi —
|
|
176
|
+
* nothing would re-enter agent_end to release it and, with no live subscription,
|
|
177
|
+
* nothing would wake it, so it would hold a backstage pane forever, reaped by
|
|
178
|
+
* nothing. Such a node is RELEASED to dormant instead (mirrors the parked-viewer
|
|
179
|
+
* release on focus-away in retargetFocus). */
|
|
160
180
|
export declare function detachToBackground(nodeId: string, pane?: string): boolean;
|
|
161
181
|
/** A reviver: resume a DORMANT node into its backstage placement (a fresh `crtr`
|
|
162
182
|
* window via reviveIntoPlacement). Injected so placement.ts need not import
|
|
@@ -267,20 +287,28 @@ export declare function recycleFocusPane(nodeId: string, pane: string, launch: R
|
|
|
267
287
|
/** §1.6 lifecycle successor — hand a truly-done focused node's viewport to its
|
|
268
288
|
* manager. Repoints the focus row `focusId` to `managerId` (a DB swap of the
|
|
269
289
|
* occupant). Two takeover realizations, split on the manager's liveness:
|
|
270
|
-
* - DORMANT manager (dead pi)
|
|
271
|
-
* woken by the finished node's `push
|
|
272
|
-
* by the external daemon INTO this
|
|
273
|
-
* (remain-on-exit), where reviveIntoPlacement's
|
|
274
|
-
* in place — no new window, no taint.
|
|
290
|
+
* - DORMANT manager (dead pi) — ONLY when it is idle + idle-release: the row
|
|
291
|
+
* repoint is all this does; the manager, woken by the finished node's `push
|
|
292
|
+
* final` landing in its inbox, is revived by the external daemon INTO this
|
|
293
|
+
* node's now-frozen focus pane (remain-on-exit), where reviveIntoPlacement's
|
|
294
|
+
* focus-pane branch resumes it in place — no new window, no taint. A dormant
|
|
295
|
+
* manager that is NOT idle-release (done/dead/canceled, or idle with another
|
|
296
|
+
* intent) is one the daemon will NEVER revive, so this returns false WITHOUT
|
|
297
|
+
* repointing — the caller then disarms the freeze (see below).
|
|
275
298
|
* - LIVE manager (pi alive in the backstage, the normal multi-child state):
|
|
276
299
|
* the daemon never revives it (it only respawns dead-pi nodes), so we must
|
|
277
300
|
* bring it into the viewport SYNCHRONOUSLY here — swap its backstage pane
|
|
278
301
|
* into the focus slot (MAJOR 1). Otherwise the manager runs off-screen
|
|
279
302
|
* forever while %m sits orphaned in the viewport and the focus row lies
|
|
280
303
|
* about LOCATION.
|
|
281
|
-
* Returns false — the caller closes the focus (Q1
|
|
282
|
-
* the
|
|
283
|
-
*
|
|
304
|
+
* Returns false — the caller closes the focus (Q1: closeFocusToShell disarms
|
|
305
|
+
* remain-on-exit so the pane REAPS on exit) — whenever NO successor will claim
|
|
306
|
+
* the frozen pane: no manager, the manager IS this node, the manager already
|
|
307
|
+
* occupies another viewport (UNIQUE node_id: do NOT move it, §1.6 edge), the
|
|
308
|
+
* manager row is missing, or it is a dormant manager the daemon won't revive
|
|
309
|
+
* (not idle-release) / a live-but-paneless inline root. It does NOT repoint the
|
|
310
|
+
* occupant on any false path — repointing while returning false would strand the
|
|
311
|
+
* pane frozen forever with no reaper (the dead-focus-pane bug).
|
|
284
312
|
*
|
|
285
313
|
* Why the live swap is NOT the forbidden self-saw: `swap-pane -d` only EXCHANGES
|
|
286
314
|
* two panes' slot positions; it never respawns or kills the finishing node's own
|
|
@@ -25,18 +25,22 @@
|
|
|
25
25
|
// The robustness contract: a manual `move-pane`/`join-pane`/`break-pane` must
|
|
26
26
|
// NEVER read as a node death. Liveness is pane-existence, not window-existence,
|
|
27
27
|
// and reconcile makes crtr follow a move instead of fighting it.
|
|
28
|
-
import {
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { getRow, getRowByPane, getNode, setPresence, openDb, openFocusRow, setFocusOccupant, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, setFocusPane, listFocuses as listFocusRows, view, } from '../canvas/index.js';
|
|
29
31
|
import { paneExists, paneLocation, paneOfWindow, windowAlive, windowOfPane, ensureSession, openNodeWindow, respawnPaneSync, respawnPaneDetached, breakPaneToSession, splitWindow, swapPaneInPlace, setRemainOnExit, closePane, currentTmux, joinPane, selectLayout, setWindowOption, switchClient, selectWindow, } from './tmux.js';
|
|
30
|
-
import { homeSessionOf, nodeSession, newNodeId } from './nodes.js';
|
|
32
|
+
import { homeSessionOf, childBackstageOf, nodeSession, newNodeId, rootOfSpine } from './nodes.js';
|
|
33
|
+
import { nodeDir } from '../canvas/paths.js';
|
|
31
34
|
import { isBusy } from './busy.js';
|
|
35
|
+
import { isPidAlive } from '../canvas/pid.js';
|
|
32
36
|
import { transition } from './lifecycle.js';
|
|
33
37
|
// Re-export the durable REVIVE-HOME read so placement is the one front door for
|
|
34
38
|
// "where does this node live."
|
|
35
|
-
export { homeSessionOf };
|
|
39
|
+
export { homeSessionOf, childBackstageOf };
|
|
36
40
|
// Placement is the sanctioned model-over-driver (§2.1): non-placement runtime /
|
|
37
41
|
// command modules that legitimately need a raw driver verb get it from here, so
|
|
38
42
|
// the §5.1 lint can hold "only placement.ts / tmux-chrome.ts import tmux.ts".
|
|
39
|
-
export { piCommand, paneLocation, currentTmux, inTmux, ensureSession, openNodeWindow, focusWindow, windowAlive, windowOfPane, respawnPane, } from './tmux.js';
|
|
43
|
+
export { piCommand, paneLocation, currentTmux, inTmux, ensureSession, openNodeWindow, splitWindow, focusWindow, windowAlive, windowOfPane, respawnPane, respawnPaneSync, setPaneOption, getPaneOption, paneCurrentPath, listLivePanes, } from './tmux.js';
|
|
40
44
|
export { nodeSession } from './nodes.js';
|
|
41
45
|
// ---------------------------------------------------------------------------
|
|
42
46
|
// Focus reads (Step 4) — COMPOSE over the canvas focuses table (§2.3/§4).
|
|
@@ -71,6 +75,27 @@ export function focusedNodes() {
|
|
|
71
75
|
export function listFocuses() {
|
|
72
76
|
return listFocusRows();
|
|
73
77
|
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Graph → focus routing (for surfacing human-in-the-loop prompts)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
/** The on-screen viewport a human-in-the-loop prompt raised by `nodeId` should
|
|
82
|
+
* surface into: the HIGHEST FOCUSED node of nodeId's graph — the focused node
|
|
83
|
+
* closest to the graph root, i.e. the session/window the user is actually
|
|
84
|
+
* watching this work in. Walks nodeId's spine to its root, enumerates the whole
|
|
85
|
+
* tree root-first (`view` is BFS ⇒ shallowest first), and returns the focus row
|
|
86
|
+
* of the first node that occupies a viewport. null when nothing in the graph is
|
|
87
|
+
* on screen — the caller then surfaces in the user's attached pane rather than
|
|
88
|
+
* the backstage node session. PURE (db reads only): no tmux probe, so the pane
|
|
89
|
+
* may be stale; the caller liveness-checks before targeting it. */
|
|
90
|
+
export function graphSurfaceTarget(nodeId) {
|
|
91
|
+
const root = rootOfSpine(nodeId);
|
|
92
|
+
for (const id of [root, ...view(root)]) {
|
|
93
|
+
const f = getFocusByNode(id);
|
|
94
|
+
if (f !== null && f.pane !== null)
|
|
95
|
+
return f;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
74
99
|
/** PURE reconciliation decision (§2.4) — unit-testable without a live tmux.
|
|
75
100
|
* Given the cached row LOCATION and what tmux currently reports, decide the
|
|
76
101
|
* presence patch. Mirrors the pure-core/impure-shell split (cf. `livenessVerdict`
|
|
@@ -209,8 +234,10 @@ export function reviveTarget(focus, focusPaneAlive, homeSession) {
|
|
|
209
234
|
* nothing else. That is the structural bug-kill.
|
|
210
235
|
*
|
|
211
236
|
* `setPresence` (the one atomic LOCATION write) records where the node landed.
|
|
212
|
-
* CRTR_ROOT_SESSION is forced to
|
|
213
|
-
* children always flow to the backstage, never
|
|
237
|
+
* CRTR_ROOT_SESSION is forced to the node's children-backstage (childBackstageOf)
|
|
238
|
+
* in BOTH branches so the node's children always flow to the backstage, never
|
|
239
|
+
* into the focus session — and, for a front-door ROOT, never into the user
|
|
240
|
+
* session its own pane adopted. */
|
|
214
241
|
export function reviveIntoPlacement(nodeId, launch) {
|
|
215
242
|
// §2.4 — follow any manual pane move before acting.
|
|
216
243
|
reconcile(nodeId);
|
|
@@ -218,9 +245,12 @@ export function reviveIntoPlacement(nodeId, launch) {
|
|
|
218
245
|
const focusPaneAlive = focus !== null && focus.pane !== null && paneExists(focus.pane);
|
|
219
246
|
const homeSession = homeSessionOf(nodeId);
|
|
220
247
|
const decision = reviveTarget(focus, focusPaneAlive, homeSession);
|
|
221
|
-
// The node's children always spawn into the backstage
|
|
222
|
-
//
|
|
223
|
-
|
|
248
|
+
// The node's children always spawn into the backstage, never the focus session
|
|
249
|
+
// — force it regardless of which branch the node itself takes. childBackstageOf
|
|
250
|
+
// is home_session for a managed child but `nodeSession()` for a root (whose
|
|
251
|
+
// home_session may be a user session it adopted), so a refreshed front-door
|
|
252
|
+
// root never re-points its subtree into the user's session.
|
|
253
|
+
const env = { ...launch.env, CRTR_ROOT_SESSION: childBackstageOf(nodeId), CRTR_SUBTREE: rootOfSpine(nodeId) };
|
|
224
254
|
if (decision.kind === 'focus-pane') {
|
|
225
255
|
// F3: resume the pi INTO the live focus pane, in its CURRENT session (Q4 —
|
|
226
256
|
// reconcileFocus follows a user move of the focus pane). No new window.
|
|
@@ -263,7 +293,15 @@ export function reviveIntoPlacement(nodeId, launch) {
|
|
|
263
293
|
* CURRENT location, §2.4) and again after (presence FOLLOWS the move). No-op
|
|
264
294
|
* (false) when there is no live pane to relocate or tmux refuses the break.
|
|
265
295
|
* `pane` is the authoritative node pane the caller acts on (the Alt+C menu's
|
|
266
|
-
* `#{pane_id}`); falls back to the node's durable handle.
|
|
296
|
+
* `#{pane_id}`); falls back to the node's durable handle.
|
|
297
|
+
*
|
|
298
|
+
* Invariant P gate (Bug 1): the backstage holds ONLY generating-but-unfocused
|
|
299
|
+
* nodes. A node that is NOT mid-turn (e.g. a resident root parked idle between
|
|
300
|
+
* turns, detached via Alt+C → D) must never be parked there as a live-idle pi —
|
|
301
|
+
* nothing would re-enter agent_end to release it and, with no live subscription,
|
|
302
|
+
* nothing would wake it, so it would hold a backstage pane forever, reaped by
|
|
303
|
+
* nothing. Such a node is RELEASED to dormant instead (mirrors the parked-viewer
|
|
304
|
+
* release on focus-away in retargetFocus). */
|
|
267
305
|
export function detachToBackground(nodeId, pane) {
|
|
268
306
|
reconcile(nodeId);
|
|
269
307
|
const row = getRow(nodeId);
|
|
@@ -276,6 +314,28 @@ export function detachToBackground(nodeId, pane) {
|
|
|
276
314
|
// follows the right pane.
|
|
277
315
|
if (row.pane !== target)
|
|
278
316
|
setPresence(nodeId, { pane: target });
|
|
317
|
+
// Invariant P (Bug 1): a node that is NOT mid-turn must never be parked in the
|
|
318
|
+
// backstage as a live-idle pi (it would hold a backstage pane forever, reaped
|
|
319
|
+
// by nothing). Detaching such a node RELEASES it to dormant instead: close its
|
|
320
|
+
// focus, then — crash-safety, as in retargetFocus's release path — flip it to
|
|
321
|
+
// dormant (idle + intent='idle-release', revivable via inbox / re-focus) and
|
|
322
|
+
// null its LOCATION BEFORE reaping the pane, so a daemon tick can never catch a
|
|
323
|
+
// window-gone live node mid-reap and race to revive it. Unlike retargetFocus's
|
|
324
|
+
// outgoingDisposition (which keeps a RESIDENT node warm on focus-away), detach
|
|
325
|
+
// is a deliberate let-go: a non-generating node is released regardless of
|
|
326
|
+
// lifecycle. The status guard skips the transition for a done/dead/canceled
|
|
327
|
+
// node ('release' is legal only from active|idle) — it still reaps + nulls.
|
|
328
|
+
if (!isGenerating(nodeId)) {
|
|
329
|
+
const f = focusOf(nodeId);
|
|
330
|
+
if (f !== null)
|
|
331
|
+
closeFocusRow(f.focus_id);
|
|
332
|
+
if (row.status === 'active' || row.status === 'idle')
|
|
333
|
+
transition(nodeId, 'release');
|
|
334
|
+
setPresence(nodeId, { pane: null, tmux_session: null, window: null });
|
|
335
|
+
closePane(target);
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
// Generating → park in the backstage, keep running off-screen.
|
|
279
339
|
const session = nodeSession();
|
|
280
340
|
ensureSession(session, row.cwd);
|
|
281
341
|
const ok = breakPaneToSession(target, session);
|
|
@@ -304,18 +364,6 @@ function holderId(focusId) {
|
|
|
304
364
|
function newFocusId() {
|
|
305
365
|
return `f-${newNodeId()}`;
|
|
306
366
|
}
|
|
307
|
-
/** signal-0 liveness probe for a pi pid (mirrors the daemon's isPidAlive). */
|
|
308
|
-
function pidAlive(pid) {
|
|
309
|
-
if (pid == null)
|
|
310
|
-
return false;
|
|
311
|
-
try {
|
|
312
|
-
process.kill(pid, 0);
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
catch (e) {
|
|
316
|
-
return e.code === 'EPERM';
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
367
|
/** Is a focus's OUTGOING occupant still GENERATING (a live pi actually MID-TURN)?
|
|
320
368
|
* A still-generating node is moved to backstage by a retarget (F2 — it keeps
|
|
321
369
|
* running off-screen); a holder / done / dormant / merely-parked node has its
|
|
@@ -327,7 +375,7 @@ function pidAlive(pid) {
|
|
|
327
375
|
* pid between turns; pid-alive would misclassify it as "doing work" and leave
|
|
328
376
|
* it stuck backstaged-active forever. `isBusy` is true only inside a turn, so a
|
|
329
377
|
* parked viewer reads as not-generating and is released to dormant on
|
|
330
|
-
* focus-away. The AND with `
|
|
378
|
+
* focus-away. The AND with `isPidAlive` makes a stale marker (a pi that crashed
|
|
331
379
|
* mid-turn) harmless. */
|
|
332
380
|
function isGenerating(nodeId) {
|
|
333
381
|
const row = getRow(nodeId);
|
|
@@ -335,7 +383,7 @@ function isGenerating(nodeId) {
|
|
|
335
383
|
return false;
|
|
336
384
|
if (row.status !== 'active' && row.status !== 'idle')
|
|
337
385
|
return false;
|
|
338
|
-
return isBusy(nodeId) &&
|
|
386
|
+
return isBusy(nodeId) && isPidAlive(row.pi_pid);
|
|
339
387
|
}
|
|
340
388
|
export function outgoingDisposition(o) {
|
|
341
389
|
if (!o.exists)
|
|
@@ -535,6 +583,18 @@ export function focus(nodeId, opts) {
|
|
|
535
583
|
const meta = getNode(nodeId);
|
|
536
584
|
if (meta === null)
|
|
537
585
|
return { focused: false, session: null, inPlace: false, revived: false };
|
|
586
|
+
// ── Phase-4 broker-focus opt-in (headless-broker model) ────────────────────
|
|
587
|
+
// A broker node has NO engine pane: headlessBrokerHost.launch spawns a
|
|
588
|
+
// DETACHED broker that hosts the single engine + binds view.sock, opening no
|
|
589
|
+
// tmux window. So focus cannot swap an engine pane into the caller's viewport
|
|
590
|
+
// — there is none. It instead (a) ensures the broker/engine is ALIVE via the
|
|
591
|
+
// sanctioned reviver, then (b) opens a tmux pane running the VIEWER
|
|
592
|
+
// `crtr attach to <id>`, which connects to view.sock. The viewer NEVER
|
|
593
|
+
// launches the engine (the one-writer invariant) — placement does, here. This
|
|
594
|
+
// is an ISOLATED opt-in BESIDE the engine-in-pane path: it fires ONLY for
|
|
595
|
+
// host_kind==='broker', so the tmux-host placement below stays byte-identical.
|
|
596
|
+
if (meta.host_kind === 'broker')
|
|
597
|
+
return focusBroker(nodeId, meta, opts);
|
|
538
598
|
const callerPane = opts.pane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
539
599
|
if (callerPane === undefined || callerPane === '') {
|
|
540
600
|
// Not in tmux — no viewport to swap into. Reconcile and report status.
|
|
@@ -564,6 +624,105 @@ export function focus(nodeId, opts) {
|
|
|
564
624
|
}
|
|
565
625
|
return retargetFocus(f.focus_id, nodeId, opts.revive);
|
|
566
626
|
}
|
|
627
|
+
const BROKER_FOCUS_SOCKET_WAIT_MS = 30_000;
|
|
628
|
+
const BROKER_FOCUS_SOCKET_RETRY_MS = 100;
|
|
629
|
+
/** Synchronously wait until a broker's view.sock accepts a connection. `focus()`
|
|
630
|
+
* is sync today (the command layer calls it directly), so the readiness probe
|
|
631
|
+
* lives in a short child Node process that can use async net events while this
|
|
632
|
+
* process blocks in `spawnSync`. Success proves more than file existence: it is
|
|
633
|
+
* robust to a stale leftover socket that the launching broker has not unlinked
|
|
634
|
+
* yet, because only an accepting listener exits 0. */
|
|
635
|
+
function waitForBrokerViewSocket(nodeId) {
|
|
636
|
+
const sockPath = join(nodeDir(nodeId), 'view.sock');
|
|
637
|
+
const probe = `
|
|
638
|
+
const net = require('node:net');
|
|
639
|
+
const sockPath = process.argv[1];
|
|
640
|
+
const deadline = Date.now() + Number(process.argv[2]);
|
|
641
|
+
const delay = Number(process.argv[3]);
|
|
642
|
+
function attempt() {
|
|
643
|
+
let socket;
|
|
644
|
+
let settled = false;
|
|
645
|
+
const finish = (ok) => {
|
|
646
|
+
if (settled) return;
|
|
647
|
+
settled = true;
|
|
648
|
+
if (socket !== undefined) socket.destroy();
|
|
649
|
+
if (ok) process.exit(0);
|
|
650
|
+
if (Date.now() >= deadline) process.exit(1);
|
|
651
|
+
setTimeout(attempt, delay);
|
|
652
|
+
};
|
|
653
|
+
try {
|
|
654
|
+
socket = net.createConnection(sockPath);
|
|
655
|
+
} catch {
|
|
656
|
+
finish(false);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
socket.once('connect', () => finish(true));
|
|
660
|
+
socket.once('error', () => finish(false));
|
|
661
|
+
socket.setTimeout(delay, () => finish(false));
|
|
662
|
+
}
|
|
663
|
+
attempt();
|
|
664
|
+
`;
|
|
665
|
+
const r = spawnSync(process.execPath, ['--input-type=commonjs', '-e', probe, sockPath, String(BROKER_FOCUS_SOCKET_WAIT_MS), String(BROKER_FOCUS_SOCKET_RETRY_MS)], {
|
|
666
|
+
stdio: 'ignore',
|
|
667
|
+
timeout: BROKER_FOCUS_SOCKET_WAIT_MS + 1_000,
|
|
668
|
+
// Keep the probe deterministic: NODE_OPTIONS like --input-type=module or
|
|
669
|
+
// --inspect-brk can break/hang a tiny `node -e` readiness check.
|
|
670
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
671
|
+
});
|
|
672
|
+
return r.status === 0;
|
|
673
|
+
}
|
|
674
|
+
/** Phase-4 broker-focus (headless-broker model) — open an attached VIEWER pane
|
|
675
|
+
* for a node whose engine runs in a DETACHED broker (host_kind==='broker'), the
|
|
676
|
+
* small opt-in beside the engine-in-pane path. Two steps:
|
|
677
|
+
* (a) ENSURE THE BROKER (engine) IS ALIVE via the sanctioned launcher — the
|
|
678
|
+
* injected reviver (reviveNode → headlessBrokerHost.launch). A broker's
|
|
679
|
+
* liveness IS its recorded pid (headlessBrokerHost.isAlive ===
|
|
680
|
+
* isPidAlive(pi_pid)); reviveNode's double-revive guard makes this
|
|
681
|
+
* idempotent: already-alive → no-op, dead → launch ONE detached broker —
|
|
682
|
+
* never a second engine. The VIEWER never does this; placement does.
|
|
683
|
+
* (b) OPEN THE VIEWER PANE — after a bounded view.sock readiness wait (closes
|
|
684
|
+
* the cold-start race against attach's single connect), split a pane
|
|
685
|
+
* beside the caller running exactly the VIEWER command `crtr attach to
|
|
686
|
+
* <id>`, which connects to view.sock and renders/drives the live engine.
|
|
687
|
+
*
|
|
688
|
+
* No FOCUS ROW is registered: a broker viewer pane is NOT an engine pane (the
|
|
689
|
+
* engine lives in the detached broker; pi_pid is the broker's), so it stays out
|
|
690
|
+
* of the focuses table / reconcile / retargetFocus, which track engine-in-pane
|
|
691
|
+
* swap semantics. (Repeated focus thus stacks viewers — accepted for the Phase-4
|
|
692
|
+
* opt-in; the Phase-5 default-swap owns any viewer dedup.) Reaches no engine
|
|
693
|
+
* launcher directly (placement must not import revive.ts/host.ts — a cycle):
|
|
694
|
+
* step (a) goes through the injected `revive`, step (b) through the existing
|
|
695
|
+
* splitWindow driver verb. `--new-pane` is moot — a broker focus is ALWAYS a
|
|
696
|
+
* fresh viewer pane — so the flag is ignored. */
|
|
697
|
+
function focusBroker(nodeId, meta, opts) {
|
|
698
|
+
const callerPane = opts.pane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
699
|
+
if (callerPane === undefined || callerPane === '') {
|
|
700
|
+
// crtr is tmux-only: no viewport to open and no non-tmux fallback.
|
|
701
|
+
return { focused: false, session: null, inPlace: false, revived: false };
|
|
702
|
+
}
|
|
703
|
+
// (a) Ensure the broker is alive (idempotent). Capture prior liveness so the
|
|
704
|
+
// FocusResult reports whether THIS focus had to launch it.
|
|
705
|
+
const wasAlive = isPidAlive(getNode(nodeId)?.pi_pid ?? null);
|
|
706
|
+
opts.revive(nodeId);
|
|
707
|
+
const revived = !wasAlive;
|
|
708
|
+
// Close the cold-start race: a freshly launched broker records pid during
|
|
709
|
+
// extension bind, then opens view.sock later; attach connects once and exits
|
|
710
|
+
// "no broker" if we split before listen(). Probe for an ACCEPTING socket (not
|
|
711
|
+
// mere path existence) before opening the viewer pane.
|
|
712
|
+
if (!waitForBrokerViewSocket(nodeId)) {
|
|
713
|
+
return { focused: false, session: null, inPlace: false, revived };
|
|
714
|
+
}
|
|
715
|
+
// Propagate CRTR_HOME so the viewer resolves the SAME canvas home (and thus
|
|
716
|
+
// the right view.sock) under a non-default override; otherwise an empty env
|
|
717
|
+
// (the split inherits the tmux server env, like every other crtr chrome pane).
|
|
718
|
+
const crtrHome = process.env['CRTR_HOME'];
|
|
719
|
+
const env = crtrHome !== undefined ? { CRTR_HOME: crtrHome } : {};
|
|
720
|
+
// Node ids are shell-safe identifiers (base36-ts + hex); no quoting needed.
|
|
721
|
+
const pane = splitWindow(callerPane, { cwd: meta.cwd, env, command: `crtr attach to ${nodeId}` });
|
|
722
|
+
if (pane === null)
|
|
723
|
+
return { focused: false, session: null, inPlace: false, revived };
|
|
724
|
+
return { focused: true, session: paneLocation(pane)?.session ?? null, inPlace: false, revived };
|
|
725
|
+
}
|
|
567
726
|
/** Register the caller's CURRENT pane as a focus so a `node focus`/`cycle` from a
|
|
568
727
|
* pane that isn't yet a viewport retargets IN PLACE. Occupied by whatever node
|
|
569
728
|
* sits in the pane now (`callerNode`, else resolved by pane→row), or a HOLDER
|
|
@@ -625,20 +784,28 @@ export function recycleFocusPane(nodeId, pane, launch) {
|
|
|
625
784
|
/** §1.6 lifecycle successor — hand a truly-done focused node's viewport to its
|
|
626
785
|
* manager. Repoints the focus row `focusId` to `managerId` (a DB swap of the
|
|
627
786
|
* occupant). Two takeover realizations, split on the manager's liveness:
|
|
628
|
-
* - DORMANT manager (dead pi)
|
|
629
|
-
* woken by the finished node's `push
|
|
630
|
-
* by the external daemon INTO this
|
|
631
|
-
* (remain-on-exit), where reviveIntoPlacement's
|
|
632
|
-
* in place — no new window, no taint.
|
|
787
|
+
* - DORMANT manager (dead pi) — ONLY when it is idle + idle-release: the row
|
|
788
|
+
* repoint is all this does; the manager, woken by the finished node's `push
|
|
789
|
+
* final` landing in its inbox, is revived by the external daemon INTO this
|
|
790
|
+
* node's now-frozen focus pane (remain-on-exit), where reviveIntoPlacement's
|
|
791
|
+
* focus-pane branch resumes it in place — no new window, no taint. A dormant
|
|
792
|
+
* manager that is NOT idle-release (done/dead/canceled, or idle with another
|
|
793
|
+
* intent) is one the daemon will NEVER revive, so this returns false WITHOUT
|
|
794
|
+
* repointing — the caller then disarms the freeze (see below).
|
|
633
795
|
* - LIVE manager (pi alive in the backstage, the normal multi-child state):
|
|
634
796
|
* the daemon never revives it (it only respawns dead-pi nodes), so we must
|
|
635
797
|
* bring it into the viewport SYNCHRONOUSLY here — swap its backstage pane
|
|
636
798
|
* into the focus slot (MAJOR 1). Otherwise the manager runs off-screen
|
|
637
799
|
* forever while %m sits orphaned in the viewport and the focus row lies
|
|
638
800
|
* about LOCATION.
|
|
639
|
-
* Returns false — the caller closes the focus (Q1
|
|
640
|
-
* the
|
|
641
|
-
*
|
|
801
|
+
* Returns false — the caller closes the focus (Q1: closeFocusToShell disarms
|
|
802
|
+
* remain-on-exit so the pane REAPS on exit) — whenever NO successor will claim
|
|
803
|
+
* the frozen pane: no manager, the manager IS this node, the manager already
|
|
804
|
+
* occupies another viewport (UNIQUE node_id: do NOT move it, §1.6 edge), the
|
|
805
|
+
* manager row is missing, or it is a dormant manager the daemon won't revive
|
|
806
|
+
* (not idle-release) / a live-but-paneless inline root. It does NOT repoint the
|
|
807
|
+
* occupant on any false path — repointing while returning false would strand the
|
|
808
|
+
* pane frozen forever with no reaper (the dead-focus-pane bug).
|
|
642
809
|
*
|
|
643
810
|
* Why the live swap is NOT the forbidden self-saw: `swap-pane -d` only EXCHANGES
|
|
644
811
|
* two panes' slot positions; it never respawns or kills the finishing node's own
|
|
@@ -654,19 +821,42 @@ export function handFocusToManager(focusId, managerId) {
|
|
|
654
821
|
return false;
|
|
655
822
|
if (getFocusByNode(managerId) !== null)
|
|
656
823
|
return false; // manager already focused elsewhere
|
|
657
|
-
setFocusOccupant(focusId, managerId);
|
|
658
|
-
// MAJOR 1 — LIVE backstage manager → swap it into the focus slot now. DORMANT
|
|
659
|
-
// managers (no live pane / dead pi) fall through unchanged: the daemon revives
|
|
660
|
-
// them into the frozen %m async.
|
|
661
824
|
const mgr = getRow(managerId);
|
|
662
|
-
if (mgr
|
|
825
|
+
if (mgr === null)
|
|
826
|
+
return false; // no row to hand to
|
|
827
|
+
// MAJOR 1 — LIVE backstage manager → swap it into the focus slot NOW. The
|
|
828
|
+
// daemon never revives a live node (it only respawns dead-pi nodes), so this
|
|
829
|
+
// synchronous swap is the ONLY way a live manager claims the frozen %m. Commit
|
|
830
|
+
// the occupant repoint only here, on a path that genuinely takes the pane.
|
|
831
|
+
if (mgr.pane != null && isNodePaneAlive(mgr) && isPidAlive(mgr.pi_pid) && f.pane != null) {
|
|
832
|
+
setFocusOccupant(focusId, managerId);
|
|
663
833
|
const focusLoc = paneLocation(f.pane); // F2's window/session — the slot mgr swaps INTO (%m is currently there)
|
|
664
834
|
if (swapPaneInPlace(mgr.pane, f.pane) && focusLoc !== null) {
|
|
665
835
|
setFocusPane(f.focus_id, mgr.pane, focusLoc.session); // re-anchor the focus row to mgr's pane (now in F2)
|
|
666
836
|
setPresence(managerId, { pane: mgr.pane, tmux_session: focusLoc.session, window: focusLoc.window });
|
|
667
837
|
}
|
|
838
|
+
return true; // took focus (live swap) — caller doesn't disarm
|
|
668
839
|
}
|
|
669
|
-
|
|
840
|
+
// DORMANT manager → it can only enter the frozen %m via crtrd.superviseTick's
|
|
841
|
+
// SECOND PASS, which revives a node ONLY when status==='idle' && intent===
|
|
842
|
+
// 'idle-release' (AND pi dead + unseen inbox). We mirror just status+intent: the
|
|
843
|
+
// other two are guaranteed by lifecycle invariants — every transition(_,
|
|
844
|
+
// 'release') site kills pi immediately, so idle-release ⟹ pi-dead; and the
|
|
845
|
+
// `push --final` that drove us into this done-branch just seeded the manager's
|
|
846
|
+
// inbox. (If a future idle-release path ever leaves pi LIVE, revisit this.)
|
|
847
|
+
// Mirror that predicate EXACTLY: a done/dead/
|
|
848
|
+
// canceled, an idle-but-not-idle-release, or a live-but-paneless (inline root)
|
|
849
|
+
// manager will NEVER be brought into this pane. Repointing for one of those
|
|
850
|
+
// makes the caller SKIP closeFocusToShell ⇒ remain-on-exit stays ON ⇒ the pane
|
|
851
|
+
// freezes forever with no reaper (the dead-focus-pane bug). So ONLY repoint +
|
|
852
|
+
// return true when the daemon will genuinely revive it; otherwise return false
|
|
853
|
+
// WITHOUT repointing so the caller disarms the freeze and the pane reaps on exit
|
|
854
|
+
// (§1.6 flow (b), close-out-completely).
|
|
855
|
+
const daemonWillRevive = mgr.status === 'idle' && mgr.intent === 'idle-release';
|
|
856
|
+
if (!daemonWillRevive)
|
|
857
|
+
return false;
|
|
858
|
+
setFocusOccupant(focusId, managerId); // repoint only; the daemon revives mgr INTO the frozen %m
|
|
859
|
+
return true;
|
|
670
860
|
}
|
|
671
861
|
/** Q1 close-to-shell for a truly-done focused node with no successor (§1.6 /
|
|
672
862
|
* flow (b)): close its focus row and DISARM the pane's
|
|
@@ -6,12 +6,6 @@ export interface PromoteResult {
|
|
|
6
6
|
roadmapPath: string;
|
|
7
7
|
/** Absolute path to the node's goal doc (context/initial-prompt.md). */
|
|
8
8
|
goalPath: string;
|
|
9
|
-
/** Absolute path to the node-local memory index (context/memory/MEMORY.md). */
|
|
10
|
-
memoryPath: string;
|
|
11
|
-
/** Absolute path to the user-global memory index (<crtrHome>/memory/MEMORY.md). */
|
|
12
|
-
userMemoryPath: string;
|
|
13
|
-
/** Absolute path to the project memory index (<crtrHome>/projects/<key>/memory/MEMORY.md). */
|
|
14
|
-
projectMemoryPath: string;
|
|
15
9
|
}
|
|
16
10
|
/** Promote a node to an orchestrator (mode→orchestrator), optionally
|
|
17
11
|
* specializing its kind (e.g. a `general` worker becoming a
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 1. Promotion → mode flips to orchestrator (mid-turn). This call flips the
|
|
5
5
|
// node's mode and (optionally) its KIND, REWRITES its launch spec to that
|
|
6
6
|
// kind's orchestrator persona (so the next revive comes back as that
|
|
7
|
-
// orchestrator), and seeds a roadmap scaffold
|
|
7
|
+
// orchestrator), and seeds a roadmap scaffold.
|
|
8
8
|
// The transition guidance the node needs is injected CENTRALLY by the
|
|
9
9
|
// persona injector (runtime/persona.ts) at the turn boundary — promote()
|
|
10
10
|
// itself no longer returns or hand-emits guidance.
|
|
@@ -22,7 +22,6 @@ import { getNode, updateNode } from '../canvas/index.js';
|
|
|
22
22
|
import { transition } from './lifecycle.js';
|
|
23
23
|
import { buildLaunchSpec } from './launch.js';
|
|
24
24
|
import { hasRoadmap, seedRoadmap, roadmapPath } from './roadmap.js';
|
|
25
|
-
import { seedMemory, memoryPath, seedUserMemory, userMemoryPath, seedProjectMemory, projectMemoryPath, } from './memory.js';
|
|
26
25
|
import { readGoal, goalPath } from './kickoff.js';
|
|
27
26
|
/** Promote a node to an orchestrator (mode→orchestrator), optionally
|
|
28
27
|
* specializing its kind (e.g. a `general` worker becoming a
|
|
@@ -58,13 +57,6 @@ export function promote(nodeId, opts = {}) {
|
|
|
58
57
|
seedRoadmap(nodeId, goal !== null && goal.trim() !== '' ? { goal: goal.trim() } : {});
|
|
59
58
|
roadmapWritten = true;
|
|
60
59
|
}
|
|
61
|
-
// Seed all three scoped memory stores alongside the roadmap — user-global,
|
|
62
|
-
// project (keyed off this node's cwd), and node-local. Each is a durable,
|
|
63
|
-
// refresh-surviving artifact; each guarded so a re-seed never clobbers an
|
|
64
|
-
// evolved memory.
|
|
65
|
-
seedUserMemory();
|
|
66
|
-
seedProjectMemory(node.cwd);
|
|
67
|
-
seedMemory(nodeId);
|
|
68
60
|
// Flip mode→orchestrator + kind + launch spec. Lifecycle is independent:
|
|
69
61
|
// only set resident when the caller asked for it (the common self-promotion
|
|
70
62
|
// stays terminal/orchestrator — it still reports up + reaps).
|
|
@@ -79,9 +71,6 @@ export function promote(nodeId, opts = {}) {
|
|
|
79
71
|
roadmapWritten,
|
|
80
72
|
roadmapPath: roadmapPath(nodeId),
|
|
81
73
|
goalPath: goalPath(nodeId),
|
|
82
|
-
memoryPath: memoryPath(nodeId),
|
|
83
|
-
userMemoryPath: userMemoryPath(),
|
|
84
|
-
projectMemoryPath: projectMemoryPath(node.cwd),
|
|
85
74
|
};
|
|
86
75
|
}
|
|
87
76
|
/** Request a refresh-yield: discard in-memory context and revive fresh against
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface RecycleResult {
|
|
2
2
|
/** True when the pane was recycled (a fresh root respawned in it). */
|
|
3
|
-
|
|
4
|
-
/** True when a `final` report was pushed for the
|
|
3
|
+
recycled: boolean;
|
|
4
|
+
/** True when a `final` report was pushed for the recycled node. */
|
|
5
5
|
finalized: boolean;
|
|
6
6
|
/** The fresh root node booted into the pane, or null on failure. */
|
|
7
7
|
newRoot: string | null;
|
|
@@ -10,5 +10,5 @@ export interface DemoteResult {
|
|
|
10
10
|
}
|
|
11
11
|
/** Finish `nodeId` and recycle its pane into a fresh root. `callerPane` is the
|
|
12
12
|
* tmux pane the agent occupies (the Alt+C menu passes it as `#{pane_id}`).
|
|
13
|
-
* Best-effort; `
|
|
14
|
-
export declare function
|
|
13
|
+
* Best-effort; `recycled:false` when there is no pane to act on. */
|
|
14
|
+
export declare function recycleNode(nodeId: string, callerPane?: string): Promise<RecycleResult>;
|
|
@@ -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
|
}
|