@crouton-kit/crouter 0.3.14 → 0.3.16
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.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +45 -67
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -99
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +129 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +196 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +266 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +178 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +334 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +105 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +205 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +81 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +24 -12
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +26 -12
- package/dist/core/runtime/launch.js +78 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +39 -1
- package/dist/core/runtime/nodes.js +69 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +299 -0
- package/dist/core/runtime/placement.js +688 -0
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +113 -20
- package/dist/core/runtime/tmux.js +221 -39
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +594 -262
- package/dist/pi-extensions/canvas-resume.d.ts +22 -0
- package/dist/pi-extensions/canvas-resume.js +173 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +340 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +2 -2
- package/dist/core/runtime/presence.d.ts +0 -38
- package/dist/core/runtime/presence.js +0 -154
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/** POSIX single-quote escaping for one shell word. */
|
|
2
2
|
export declare function shellQuote(s: string): string;
|
|
3
3
|
export declare function inTmux(): boolean;
|
|
4
|
-
/** The single, shared tmux session that ALL canvas node windows live in.
|
|
5
|
-
* Overridable with CRTR_NODE_SESSION (default `crtr`). Every root and every
|
|
6
|
-
* child opens a window here rather than cluttering the user's own working
|
|
7
|
-
* session — switch to it to browse the whole live graph, ignore it otherwise. */
|
|
8
|
-
export declare function nodeSession(): string;
|
|
9
4
|
export interface TmuxLocation {
|
|
10
5
|
session: string;
|
|
11
6
|
window: string;
|
|
@@ -28,7 +23,8 @@ export interface OpenWindowOpts {
|
|
|
28
23
|
}
|
|
29
24
|
/** Open a background window for a node and run `command` in it. `-d` keeps it
|
|
30
25
|
* detached so it doesn't steal focus or become the current window. Returns the
|
|
31
|
-
* new window id
|
|
26
|
+
* new window id AND the pane id it created (the durable `%pane_id`, LOCATION's
|
|
27
|
+
* anchor) — callers that only need the window destructure `.window`.
|
|
32
28
|
*
|
|
33
29
|
* Target is `${session}:` (trailing colon = the session, no window index) plus
|
|
34
30
|
* `-a` (insert after the current window) so tmux allocates the next free index.
|
|
@@ -37,12 +33,41 @@ export interface OpenWindowOpts {
|
|
|
37
33
|
* "create window failed: index N in use" whenever the active window is not the
|
|
38
34
|
* last one (common when base-index is 0 but the live window sits at index 1).
|
|
39
35
|
* `-a` also keeps node windows off index 0, which is reserved for the optional
|
|
40
|
-
* dashboard.
|
|
41
|
-
|
|
36
|
+
* dashboard. The explicit `-t ${session}:` target is the §2.2 HARD DRIVER
|
|
37
|
+
* INVARIANT — never let new-window fall back to tmux's global current session. */
|
|
38
|
+
export declare function openNodeWindow(opts: OpenWindowOpts): {
|
|
39
|
+
window: string;
|
|
40
|
+
pane: string;
|
|
41
|
+
} | null;
|
|
42
|
+
export interface SplitWindowOpts {
|
|
43
|
+
cwd: string;
|
|
44
|
+
env: Record<string, string>;
|
|
45
|
+
/** The full command to run in the new pane (already a shell string). */
|
|
46
|
+
command: string;
|
|
47
|
+
/** Stack the new pane below instead of beside (default: beside, `-h`). */
|
|
48
|
+
vertical?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Split `targetPane`'s window, opening a NEW pane beside it running `command`,
|
|
51
|
+
* and return the new pane id (the durable `%id`). The ONLY new-pane-beside verb
|
|
52
|
+
* (Q3: a focus opened side-by-side). `-d` keeps the caller's pane active; `-h`
|
|
53
|
+
* makes the split side-by-side (left/right), the default for a focus viewport.
|
|
54
|
+
*
|
|
55
|
+
* §2.2 HARD DRIVER INVARIANT: `targetPane` is REQUIRED — a bare `split-window`
|
|
56
|
+
* would split tmux's global current pane, which can leak a pane into an
|
|
57
|
+
* unrelated user session (the exact bug this design kills). The explicit
|
|
58
|
+
* `-t <targetPane>` makes the destination structurally un-leakable. Returns
|
|
59
|
+
* null if tmux fails. */
|
|
60
|
+
export declare function splitWindow(targetPane: string, opts: SplitWindowOpts): string | null;
|
|
42
61
|
/** Bring a node's window forefront. Switches client across roots when needed. */
|
|
43
62
|
export declare function focusWindow(session: string, window: string): boolean;
|
|
44
63
|
/** Close a node's window (drop it from the UI). */
|
|
45
64
|
export declare function closeWindow(window: string): boolean;
|
|
65
|
+
/** Close a single PANE. Its window closes automatically once this was the last
|
|
66
|
+
* pane, but sibling panes survive — so co-located nodes (several agents sharing
|
|
67
|
+
* one window via swap-pane focus) are torn down one at a time instead of all
|
|
68
|
+
* at once by a window kill. Pane ids are the stable vehicle handle; windows
|
|
69
|
+
* shift under swap-pane focus, so pane-granular teardown is the correct unit. */
|
|
70
|
+
export declare function closePane(pane: string): boolean;
|
|
46
71
|
/** The active pane id of a window. Node windows are single-pane, so this is the
|
|
47
72
|
* node's pane. Returns null if the window is gone or tmux fails. */
|
|
48
73
|
export declare function paneOfWindow(session: string, window: string): string | null;
|
|
@@ -51,12 +76,35 @@ export declare function paneOfWindow(session: string, window: string): string |
|
|
|
51
76
|
* are not, so the node→window mapping must be re-derived from the pane. Returns
|
|
52
77
|
* null if the pane is gone or tmux fails. */
|
|
53
78
|
export declare function windowOfPane(pane: string): string | null;
|
|
54
|
-
/** The session + window a pane currently lives in
|
|
55
|
-
*
|
|
79
|
+
/** The session + window a pane currently lives in (`display-message -p -t %id`).
|
|
80
|
+
* The §2.4 reconciliation read-back: resolve a node's/focus's CURRENT
|
|
81
|
+
* window/session from its durable pane id before any act, so crtr follows a
|
|
82
|
+
* manual `move-pane`/`join-pane`/`break-pane` instead of fighting it. Null if
|
|
83
|
+
* the pane is gone or tmux fails. */
|
|
56
84
|
export declare function paneLocation(pane: string): {
|
|
57
85
|
session: string;
|
|
58
86
|
window: string;
|
|
59
87
|
} | null;
|
|
88
|
+
/** Does this pane id still exist? A `display-message` probe on the `%id` — the
|
|
89
|
+
* v3 PRIMARY liveness probe (§1.2/§2.2), replacing window-existence so a user
|
|
90
|
+
* moving a pane to another window/session never reads as "gone". True iff tmux
|
|
91
|
+
* knows the pane.
|
|
92
|
+
*
|
|
93
|
+
* NOTE: `display-message -p -t <gone-pane>` EXITS 0 with EMPTY output (it does
|
|
94
|
+
* not error on an unresolvable pane target) — so an `.ok` check alone would
|
|
95
|
+
* report a dead pane as alive, defeating the whole point of pane-existence
|
|
96
|
+
* liveness. We therefore require the echoed `#{pane_id}` to equal the requested
|
|
97
|
+
* pane: a live pane echoes its own id, a gone/bogus one yields empty. */
|
|
98
|
+
export declare function paneExists(pane: string): boolean;
|
|
99
|
+
/** Relocate a pane into another session as its own window WITHOUT killing the
|
|
100
|
+
* process in it — `break-pane -d` moves the pane out of its current window (the
|
|
101
|
+
* pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
|
|
102
|
+
* client where it is rather than following the pane to the background, and `-a`
|
|
103
|
+
* allocates the next free window index (same dodge as openNodeWindow). The
|
|
104
|
+
* "detach to background" driver behind `node lifecycle --detach`. Best-effort;
|
|
105
|
+
* false if tmux refuses (e.g. the pane is gone). The caller reconciles presence
|
|
106
|
+
* so the canvas follows the move. */
|
|
107
|
+
export declare function breakPaneToSession(pane: string, session: string): boolean;
|
|
60
108
|
/** Swap `targetPane` into `callerPane`'s layout slot, IN PLACE. `-d` keeps the
|
|
61
109
|
* caller's window active, so the target's pane appears where the caller is
|
|
62
110
|
* rather than navigating the client off to the target's window. The caller's
|
|
@@ -71,16 +119,34 @@ export interface RespawnPaneOpts {
|
|
|
71
119
|
/** The full command to run in the pane (already a shell string). */
|
|
72
120
|
command: string;
|
|
73
121
|
}
|
|
74
|
-
/** Re-exec a command in an EXISTING pane, in place.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
122
|
+
/** Re-exec a command in an EXISTING pane, in place — DETACHED. Spawned in its own
|
|
123
|
+
* process group (unref'd) so the request reaches the tmux server even though
|
|
124
|
+
* `-k` tears down the caller's own pi mid-flight. Used when a node respawns ITS
|
|
125
|
+
* OWN pane (refresh-yield): the dispatch can't be awaited because it kills the
|
|
126
|
+
* awaiter. Returns true once the request was dispatched. */
|
|
127
|
+
export declare function respawnPaneDetached(opts: RespawnPaneOpts): boolean;
|
|
128
|
+
/** Re-exec a command in an EXISTING pane, in place — SYNCHRONOUS. Runs the
|
|
129
|
+
* `respawn-pane` to completion and reports the real exit status. Used when the
|
|
130
|
+
* caller is NOT the pane being respawned (e.g. the daemon resuming a frozen
|
|
131
|
+
* focus pane), so it can confirm the respawn landed. Returns true on success. */
|
|
132
|
+
export declare function respawnPaneSync(opts: RespawnPaneOpts): boolean;
|
|
133
|
+
/** @deprecated Use respawnPaneDetached. Retained so existing refresh-yield
|
|
134
|
+
* callers stay green while the placement layer migrates onto the explicit
|
|
135
|
+
* sync/detached split. */
|
|
82
136
|
export declare function respawnPane(opts: RespawnPaneOpts): boolean;
|
|
83
|
-
/** Turn a pi argv array into a single shell command string.
|
|
137
|
+
/** Turn a pi argv array into a single shell command string.
|
|
138
|
+
*
|
|
139
|
+
* The binary defaults to `CRTR_PI_BINARY` when that env var is set, else the
|
|
140
|
+
* literal `pi`. This is a TEST-ONLY substitution seam: when CRTR_PI_BINARY is
|
|
141
|
+
* unset (every production path) the behavior is byte-identical to exec'ing
|
|
142
|
+
* `pi`. The integration-test harness points it at a deterministic fake-pi
|
|
143
|
+
* vehicle so a real `crtr node new` reaches the fake instead of the LLM `pi`,
|
|
144
|
+
* without any dependence on tmux/shell PATH inheritance — the substitution is
|
|
145
|
+
* baked into the command string at build time, in the process that calls
|
|
146
|
+
* piCommand. An explicit `binary` arg still overrides the env (no caller passes
|
|
147
|
+
* one today). The value may be a multi-word launcher (e.g. `node --import
|
|
148
|
+
* tsx/esm host.ts`); only the argv entries are shell-quoted, so a multi-word
|
|
149
|
+
* binary is spliced verbatim ahead of them. */
|
|
84
150
|
export declare function piCommand(argv: string[], binary?: string): string;
|
|
85
151
|
/** List all window ids present in `session`. Returns [] if the session does
|
|
86
152
|
* not exist or tmux fails for any reason. Each entry is the raw window id
|
|
@@ -97,7 +163,34 @@ export declare function selectWindow(session: string, window: string): boolean;
|
|
|
97
163
|
* `tmux switch-client -t <session>`. Best-effort; never throws. The caller is
|
|
98
164
|
* responsible for following up with selectWindow to land on the right window. */
|
|
99
165
|
export declare function switchClient(session: string): boolean;
|
|
100
|
-
/**
|
|
166
|
+
/** Move a source pane into a destination window (`tmux join-pane`). The source
|
|
167
|
+
* pane's running process (e.g. a child's live pi) is preserved; its now-empty
|
|
168
|
+
* source window auto-closes. Best-effort; false if tmux fails. */
|
|
169
|
+
export declare function joinPane(srcPane: string, dstWindow: string): boolean;
|
|
170
|
+
/** Apply a named tmux layout to a window (`tmux select-layout`). Use
|
|
171
|
+
* `main-vertical` for one wide pane on the left + the rest stacked right.
|
|
172
|
+
* Best-effort; never throws. */
|
|
173
|
+
export declare function selectLayout(window: string, layout: string): boolean;
|
|
174
|
+
/** Set a tmux window option (`tmux set-window-option`). Used to size the main
|
|
175
|
+
* pane (`main-pane-width`) before a main-vertical layout. Best-effort. */
|
|
176
|
+
export declare function setWindowOption(window: string, name: string, value: string): boolean;
|
|
177
|
+
/** Toggle `remain-on-exit` on a window (§1.5 F3). `on` keeps a focus pane on
|
|
178
|
+
* screen after its pi exits — the viewport survives (F1), the final transcript
|
|
179
|
+
* is preserved, and `respawn-pane -k` can resurrect the node into the SAME pane
|
|
180
|
+
* id. NOTE (§1.5/§2.5, spike-confirmed): a dead/frozen pane is reaped only by
|
|
181
|
+
* `kill-pane`/`respawn-pane`, NEVER by toggling this off — the toggle does not
|
|
182
|
+
* reap an already-dead pane. Best-effort; never throws. */
|
|
183
|
+
export declare function setRemainOnExit(window: string, on: boolean): boolean;
|
|
184
|
+
/** Type a literal (e.g. a `/graph` slash command) into a pane and press Enter
|
|
185
|
+
* (`tmux send-keys -t <pane> '<text>' Enter`). Requires the pane's editor be
|
|
186
|
+
* empty, same limitation as the menu's `/promote` item. Best-effort. */
|
|
187
|
+
export declare function sendKeysEnter(pane: string, text: string): boolean;
|
|
188
|
+
/** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
|
|
189
|
+
* The built-in items (promote/demote/detach/close/browse) are static; the canvas-nav
|
|
190
|
+
* chords (graph/manager/expand/report-N + any custom prefixBind) are appended
|
|
191
|
+
* from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
|
|
192
|
+
* the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
|
|
193
|
+
* while behaviour is config-driven. */
|
|
101
194
|
export declare function installMenuBinding(): boolean;
|
|
102
195
|
/** Bind Alt+] (forward) and Alt+[ (back) to the DFS canvas walk. Best-effort;
|
|
103
196
|
* false if either bind fails. NOTE: Alt+[ is only delivered cleanly when the
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// root) or switch-client + select-window (across roots). done/dead nodes close
|
|
9
9
|
// their window; reviving opens a fresh one.
|
|
10
10
|
import { spawn, spawnSync } from 'node:child_process';
|
|
11
|
+
import { readConfig } from '../config.js';
|
|
12
|
+
import { nodeSession } from './nodes.js';
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
12
14
|
// Shell quoting + tmux invocation
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
@@ -26,14 +28,6 @@ function tmux(args) {
|
|
|
26
28
|
export function inTmux() {
|
|
27
29
|
return process.env['TMUX'] !== undefined && process.env['TMUX'] !== '';
|
|
28
30
|
}
|
|
29
|
-
/** The single, shared tmux session that ALL canvas node windows live in.
|
|
30
|
-
* Overridable with CRTR_NODE_SESSION (default `crtr`). Every root and every
|
|
31
|
-
* child opens a window here rather than cluttering the user's own working
|
|
32
|
-
* session — switch to it to browse the whole live graph, ignore it otherwise. */
|
|
33
|
-
export function nodeSession() {
|
|
34
|
-
const v = process.env['CRTR_NODE_SESSION'];
|
|
35
|
-
return v !== undefined && v !== '' ? v : 'crtr';
|
|
36
|
-
}
|
|
37
31
|
/** Where the caller currently is, or null if not inside tmux. */
|
|
38
32
|
export function currentTmux() {
|
|
39
33
|
if (!inTmux())
|
|
@@ -69,7 +63,8 @@ function envFlags(env) {
|
|
|
69
63
|
}
|
|
70
64
|
/** Open a background window for a node and run `command` in it. `-d` keeps it
|
|
71
65
|
* detached so it doesn't steal focus or become the current window. Returns the
|
|
72
|
-
* new window id
|
|
66
|
+
* new window id AND the pane id it created (the durable `%pane_id`, LOCATION's
|
|
67
|
+
* anchor) — callers that only need the window destructure `.window`.
|
|
73
68
|
*
|
|
74
69
|
* Target is `${session}:` (trailing colon = the session, no window index) plus
|
|
75
70
|
* `-a` (insert after the current window) so tmux allocates the next free index.
|
|
@@ -78,7 +73,8 @@ function envFlags(env) {
|
|
|
78
73
|
* "create window failed: index N in use" whenever the active window is not the
|
|
79
74
|
* last one (common when base-index is 0 but the live window sits at index 1).
|
|
80
75
|
* `-a` also keeps node windows off index 0, which is reserved for the optional
|
|
81
|
-
* dashboard.
|
|
76
|
+
* dashboard. The explicit `-t ${session}:` target is the §2.2 HARD DRIVER
|
|
77
|
+
* INVARIANT — never let new-window fall back to tmux's global current session. */
|
|
82
78
|
export function openNodeWindow(opts) {
|
|
83
79
|
const r = tmux([
|
|
84
80
|
'new-window',
|
|
@@ -86,7 +82,7 @@ export function openNodeWindow(opts) {
|
|
|
86
82
|
'-a',
|
|
87
83
|
'-P',
|
|
88
84
|
'-F',
|
|
89
|
-
'#{window_id}',
|
|
85
|
+
'#{window_id}\t#{pane_id}',
|
|
90
86
|
'-t',
|
|
91
87
|
`${opts.session}:`,
|
|
92
88
|
'-n',
|
|
@@ -96,7 +92,40 @@ export function openNodeWindow(opts) {
|
|
|
96
92
|
...envFlags(opts.env),
|
|
97
93
|
opts.command,
|
|
98
94
|
]);
|
|
99
|
-
|
|
95
|
+
if (!r.ok)
|
|
96
|
+
return null;
|
|
97
|
+
const [window, pane] = r.stdout.split('\t');
|
|
98
|
+
if (window === undefined || window === '' || pane === undefined || pane === '') {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return { window, pane };
|
|
102
|
+
}
|
|
103
|
+
/** Split `targetPane`'s window, opening a NEW pane beside it running `command`,
|
|
104
|
+
* and return the new pane id (the durable `%id`). The ONLY new-pane-beside verb
|
|
105
|
+
* (Q3: a focus opened side-by-side). `-d` keeps the caller's pane active; `-h`
|
|
106
|
+
* makes the split side-by-side (left/right), the default for a focus viewport.
|
|
107
|
+
*
|
|
108
|
+
* §2.2 HARD DRIVER INVARIANT: `targetPane` is REQUIRED — a bare `split-window`
|
|
109
|
+
* would split tmux's global current pane, which can leak a pane into an
|
|
110
|
+
* unrelated user session (the exact bug this design kills). The explicit
|
|
111
|
+
* `-t <targetPane>` makes the destination structurally un-leakable. Returns
|
|
112
|
+
* null if tmux fails. */
|
|
113
|
+
export function splitWindow(targetPane, opts) {
|
|
114
|
+
const r = tmux([
|
|
115
|
+
'split-window',
|
|
116
|
+
'-d',
|
|
117
|
+
...(opts.vertical === true ? [] : ['-h']),
|
|
118
|
+
'-P',
|
|
119
|
+
'-F',
|
|
120
|
+
'#{pane_id}',
|
|
121
|
+
'-t',
|
|
122
|
+
targetPane,
|
|
123
|
+
'-c',
|
|
124
|
+
opts.cwd,
|
|
125
|
+
...envFlags(opts.env),
|
|
126
|
+
opts.command,
|
|
127
|
+
]);
|
|
128
|
+
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
100
129
|
}
|
|
101
130
|
/** Bring a node's window forefront. Switches client across roots when needed. */
|
|
102
131
|
export function focusWindow(session, window) {
|
|
@@ -112,6 +141,14 @@ export function focusWindow(session, window) {
|
|
|
112
141
|
export function closeWindow(window) {
|
|
113
142
|
return tmux(['kill-window', '-t', window]).ok;
|
|
114
143
|
}
|
|
144
|
+
/** Close a single PANE. Its window closes automatically once this was the last
|
|
145
|
+
* pane, but sibling panes survive — so co-located nodes (several agents sharing
|
|
146
|
+
* one window via swap-pane focus) are torn down one at a time instead of all
|
|
147
|
+
* at once by a window kill. Pane ids are the stable vehicle handle; windows
|
|
148
|
+
* shift under swap-pane focus, so pane-granular teardown is the correct unit. */
|
|
149
|
+
export function closePane(pane) {
|
|
150
|
+
return tmux(['kill-pane', '-t', pane]).ok;
|
|
151
|
+
}
|
|
115
152
|
/** The active pane id of a window. Node windows are single-pane, so this is the
|
|
116
153
|
* node's pane. Returns null if the window is gone or tmux fails. */
|
|
117
154
|
export function paneOfWindow(session, window) {
|
|
@@ -126,8 +163,11 @@ export function windowOfPane(pane) {
|
|
|
126
163
|
const r = tmux(['display-message', '-p', '-t', pane, '#{window_id}']);
|
|
127
164
|
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
128
165
|
}
|
|
129
|
-
/** The session + window a pane currently lives in
|
|
130
|
-
*
|
|
166
|
+
/** The session + window a pane currently lives in (`display-message -p -t %id`).
|
|
167
|
+
* The §2.4 reconciliation read-back: resolve a node's/focus's CURRENT
|
|
168
|
+
* window/session from its durable pane id before any act, so crtr follows a
|
|
169
|
+
* manual `move-pane`/`join-pane`/`break-pane` instead of fighting it. Null if
|
|
170
|
+
* the pane is gone or tmux fails. */
|
|
131
171
|
export function paneLocation(pane) {
|
|
132
172
|
const r = tmux(['display-message', '-p', '-t', pane, '#{session_name}\t#{window_id}']);
|
|
133
173
|
if (!r.ok)
|
|
@@ -137,6 +177,31 @@ export function paneLocation(pane) {
|
|
|
137
177
|
return null;
|
|
138
178
|
return { session, window };
|
|
139
179
|
}
|
|
180
|
+
/** Does this pane id still exist? A `display-message` probe on the `%id` — the
|
|
181
|
+
* v3 PRIMARY liveness probe (§1.2/§2.2), replacing window-existence so a user
|
|
182
|
+
* moving a pane to another window/session never reads as "gone". True iff tmux
|
|
183
|
+
* knows the pane.
|
|
184
|
+
*
|
|
185
|
+
* NOTE: `display-message -p -t <gone-pane>` EXITS 0 with EMPTY output (it does
|
|
186
|
+
* not error on an unresolvable pane target) — so an `.ok` check alone would
|
|
187
|
+
* report a dead pane as alive, defeating the whole point of pane-existence
|
|
188
|
+
* liveness. We therefore require the echoed `#{pane_id}` to equal the requested
|
|
189
|
+
* pane: a live pane echoes its own id, a gone/bogus one yields empty. */
|
|
190
|
+
export function paneExists(pane) {
|
|
191
|
+
const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}']);
|
|
192
|
+
return r.ok && r.stdout === pane;
|
|
193
|
+
}
|
|
194
|
+
/** Relocate a pane into another session as its own window WITHOUT killing the
|
|
195
|
+
* process in it — `break-pane -d` moves the pane out of its current window (the
|
|
196
|
+
* pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
|
|
197
|
+
* client where it is rather than following the pane to the background, and `-a`
|
|
198
|
+
* allocates the next free window index (same dodge as openNodeWindow). The
|
|
199
|
+
* "detach to background" driver behind `node lifecycle --detach`. Best-effort;
|
|
200
|
+
* false if tmux refuses (e.g. the pane is gone). The caller reconciles presence
|
|
201
|
+
* so the canvas follows the move. */
|
|
202
|
+
export function breakPaneToSession(pane, session) {
|
|
203
|
+
return tmux(['break-pane', '-d', '-a', '-s', pane, '-t', `${session}:`]).ok;
|
|
204
|
+
}
|
|
140
205
|
/** Swap `targetPane` into `callerPane`'s layout slot, IN PLACE. `-d` keeps the
|
|
141
206
|
* caller's window active, so the target's pane appears where the caller is
|
|
142
207
|
* rather than navigating the client off to the target's window. The caller's
|
|
@@ -147,26 +212,34 @@ export function swapPaneInPlace(targetPane, callerPane) {
|
|
|
147
212
|
return true;
|
|
148
213
|
return tmux(['swap-pane', '-d', '-s', targetPane, '-t', callerPane]).ok;
|
|
149
214
|
}
|
|
150
|
-
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
215
|
+
/** The `respawn-pane -k` argv for `opts`. `-k` kills the pane's current process
|
|
216
|
+
* (e.g. a yielding pi) and re-execs `command` in the SAME pane, preserving its
|
|
217
|
+
* `%id` (§1.5 F3: a frozen focus pane resumes in place, no new window). The
|
|
218
|
+
* explicit `-t opts.pane` is the §2.2 HARD DRIVER INVARIANT — respawn must name
|
|
219
|
+
* its target pane, never tmux's global current pane. */
|
|
220
|
+
function respawnPaneArgs(opts) {
|
|
221
|
+
return [
|
|
222
|
+
'respawn-pane',
|
|
223
|
+
'-k',
|
|
224
|
+
'-c',
|
|
225
|
+
opts.cwd,
|
|
226
|
+
...envFlags(opts.env),
|
|
227
|
+
'-t',
|
|
228
|
+
opts.pane,
|
|
229
|
+
opts.command,
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
/** Re-exec a command in an EXISTING pane, in place — DETACHED. Spawned in its own
|
|
233
|
+
* process group (unref'd) so the request reaches the tmux server even though
|
|
234
|
+
* `-k` tears down the caller's own pi mid-flight. Used when a node respawns ITS
|
|
235
|
+
* OWN pane (refresh-yield): the dispatch can't be awaited because it kills the
|
|
236
|
+
* awaiter. Returns true once the request was dispatched. */
|
|
237
|
+
export function respawnPaneDetached(opts) {
|
|
159
238
|
try {
|
|
160
|
-
const child = spawn('tmux',
|
|
161
|
-
|
|
162
|
-
'
|
|
163
|
-
|
|
164
|
-
opts.cwd,
|
|
165
|
-
...envFlags(opts.env),
|
|
166
|
-
'-t',
|
|
167
|
-
opts.pane,
|
|
168
|
-
opts.command,
|
|
169
|
-
], { detached: true, stdio: 'ignore' });
|
|
239
|
+
const child = spawn('tmux', respawnPaneArgs(opts), {
|
|
240
|
+
detached: true,
|
|
241
|
+
stdio: 'ignore',
|
|
242
|
+
});
|
|
170
243
|
child.unref();
|
|
171
244
|
return true;
|
|
172
245
|
}
|
|
@@ -174,11 +247,36 @@ export function respawnPane(opts) {
|
|
|
174
247
|
return false;
|
|
175
248
|
}
|
|
176
249
|
}
|
|
250
|
+
/** Re-exec a command in an EXISTING pane, in place — SYNCHRONOUS. Runs the
|
|
251
|
+
* `respawn-pane` to completion and reports the real exit status. Used when the
|
|
252
|
+
* caller is NOT the pane being respawned (e.g. the daemon resuming a frozen
|
|
253
|
+
* focus pane), so it can confirm the respawn landed. Returns true on success. */
|
|
254
|
+
export function respawnPaneSync(opts) {
|
|
255
|
+
return tmux(respawnPaneArgs(opts)).ok;
|
|
256
|
+
}
|
|
257
|
+
/** @deprecated Use respawnPaneDetached. Retained so existing refresh-yield
|
|
258
|
+
* callers stay green while the placement layer migrates onto the explicit
|
|
259
|
+
* sync/detached split. */
|
|
260
|
+
export function respawnPane(opts) {
|
|
261
|
+
return respawnPaneDetached(opts);
|
|
262
|
+
}
|
|
177
263
|
// ---------------------------------------------------------------------------
|
|
178
264
|
// pi command assembly
|
|
179
265
|
// ---------------------------------------------------------------------------
|
|
180
|
-
/** Turn a pi argv array into a single shell command string.
|
|
181
|
-
|
|
266
|
+
/** Turn a pi argv array into a single shell command string.
|
|
267
|
+
*
|
|
268
|
+
* The binary defaults to `CRTR_PI_BINARY` when that env var is set, else the
|
|
269
|
+
* literal `pi`. This is a TEST-ONLY substitution seam: when CRTR_PI_BINARY is
|
|
270
|
+
* unset (every production path) the behavior is byte-identical to exec'ing
|
|
271
|
+
* `pi`. The integration-test harness points it at a deterministic fake-pi
|
|
272
|
+
* vehicle so a real `crtr node new` reaches the fake instead of the LLM `pi`,
|
|
273
|
+
* without any dependence on tmux/shell PATH inheritance — the substitution is
|
|
274
|
+
* baked into the command string at build time, in the process that calls
|
|
275
|
+
* piCommand. An explicit `binary` arg still overrides the env (no caller passes
|
|
276
|
+
* one today). The value may be a multi-word launcher (e.g. `node --import
|
|
277
|
+
* tsx/esm host.ts`); only the argv entries are shell-quoted, so a multi-word
|
|
278
|
+
* binary is spliced verbatim ahead of them. */
|
|
279
|
+
export function piCommand(argv, binary = process.env['CRTR_PI_BINARY'] ?? 'pi') {
|
|
182
280
|
return [binary, ...argv.map(shellQuote)].join(' ');
|
|
183
281
|
}
|
|
184
282
|
// ---------------------------------------------------------------------------
|
|
@@ -202,7 +300,7 @@ export function windowAlive(session, window) {
|
|
|
202
300
|
return listWindowIds(session).includes(window);
|
|
203
301
|
}
|
|
204
302
|
// ---------------------------------------------------------------------------
|
|
205
|
-
// Focus helpers (used by the
|
|
303
|
+
// Focus helpers (used by the placement layer)
|
|
206
304
|
// ---------------------------------------------------------------------------
|
|
207
305
|
/** Activate a window within its session (same-session navigation). Equivalent
|
|
208
306
|
* to `tmux select-window -t <session>:<window>`. Best-effort; never throws. */
|
|
@@ -216,12 +314,55 @@ export function switchClient(session) {
|
|
|
216
314
|
return tmux(['switch-client', '-t', session]).ok;
|
|
217
315
|
}
|
|
218
316
|
// ---------------------------------------------------------------------------
|
|
317
|
+
// Multi-pane layout (used by `canvas tmux-spread`)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
/** Move a source pane into a destination window (`tmux join-pane`). The source
|
|
320
|
+
* pane's running process (e.g. a child's live pi) is preserved; its now-empty
|
|
321
|
+
* source window auto-closes. Best-effort; false if tmux fails. */
|
|
322
|
+
export function joinPane(srcPane, dstWindow) {
|
|
323
|
+
return tmux(['join-pane', '-s', srcPane, '-t', dstWindow]).ok;
|
|
324
|
+
}
|
|
325
|
+
/** Apply a named tmux layout to a window (`tmux select-layout`). Use
|
|
326
|
+
* `main-vertical` for one wide pane on the left + the rest stacked right.
|
|
327
|
+
* Best-effort; never throws. */
|
|
328
|
+
export function selectLayout(window, layout) {
|
|
329
|
+
return tmux(['select-layout', '-t', window, layout]).ok;
|
|
330
|
+
}
|
|
331
|
+
/** Set a tmux window option (`tmux set-window-option`). Used to size the main
|
|
332
|
+
* pane (`main-pane-width`) before a main-vertical layout. Best-effort. */
|
|
333
|
+
export function setWindowOption(window, name, value) {
|
|
334
|
+
return tmux(['set-window-option', '-t', window, name, value]).ok;
|
|
335
|
+
}
|
|
336
|
+
/** Toggle `remain-on-exit` on a window (§1.5 F3). `on` keeps a focus pane on
|
|
337
|
+
* screen after its pi exits — the viewport survives (F1), the final transcript
|
|
338
|
+
* is preserved, and `respawn-pane -k` can resurrect the node into the SAME pane
|
|
339
|
+
* id. NOTE (§1.5/§2.5, spike-confirmed): a dead/frozen pane is reaped only by
|
|
340
|
+
* `kill-pane`/`respawn-pane`, NEVER by toggling this off — the toggle does not
|
|
341
|
+
* reap an already-dead pane. Best-effort; never throws. */
|
|
342
|
+
export function setRemainOnExit(window, on) {
|
|
343
|
+
return tmux(['set-window-option', '-t', window, 'remain-on-exit', on ? 'on' : 'off']).ok;
|
|
344
|
+
}
|
|
345
|
+
/** Type a literal (e.g. a `/graph` slash command) into a pane and press Enter
|
|
346
|
+
* (`tmux send-keys -t <pane> '<text>' Enter`). Requires the pane's editor be
|
|
347
|
+
* empty, same limitation as the menu's `/promote` item. Best-effort. */
|
|
348
|
+
export function sendKeysEnter(pane, text) {
|
|
349
|
+
return tmux(['send-keys', '-t', pane, text, 'Enter']).ok;
|
|
350
|
+
}
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
219
352
|
// Prefix menu — Alt+C opens a which-key-style tmux display-menu of crouter
|
|
220
353
|
// actions. Installed on the running server at root boot; idempotent (a re-bind
|
|
221
354
|
// overwrites the previous one). Items shell out to `crtr`, passing the active
|
|
222
355
|
// pane so an action targets the agent currently in front of you.
|
|
223
356
|
// ---------------------------------------------------------------------------
|
|
224
|
-
/**
|
|
357
|
+
/** Reserved mnemonic keys owned by the built-in menu items below — a custom
|
|
358
|
+
* `prefixBind` may not claim these (the built-in item wins). */
|
|
359
|
+
const RESERVED_MENU_KEYS = new Set(['o', 'r', 'd', 'D', 'x', 'b']);
|
|
360
|
+
/** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
|
|
361
|
+
* The built-in items (promote/demote/detach/close/browse) are static; the canvas-nav
|
|
362
|
+
* chords (graph/manager/expand/report-N + any custom prefixBind) are appended
|
|
363
|
+
* from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
|
|
364
|
+
* the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
|
|
365
|
+
* while behaviour is config-driven. */
|
|
225
366
|
export function installMenuBinding() {
|
|
226
367
|
const sess = nodeSession();
|
|
227
368
|
const title = ' crtr ';
|
|
@@ -230,9 +371,50 @@ export function installMenuBinding() {
|
|
|
230
371
|
// the slash command delivers the orchestration guidance into the node's
|
|
231
372
|
// context, which a bare `run-shell` (output discarded) could not.
|
|
232
373
|
{ name: 'promote to orchestrator', key: 'o', cmd: `send-keys -t '#{pane_id}' '/promote' Enter` },
|
|
233
|
-
|
|
234
|
-
|
|
374
|
+
// Resume types `/resume-node` into the agent's pane: the slash command opens
|
|
375
|
+
// a whole-canvas picker (incl. dormant nodes) and revives the choice via
|
|
376
|
+
// `crtr node focus` — the only sync-safe open (routes through reviveNode).
|
|
377
|
+
{ name: 'resume node', key: 'r', cmd: `send-keys -t '#{pane_id}' '/resume-node' Enter` },
|
|
378
|
+
// `d` demotes the agent to TERMINAL in place: no finalize, no kill — it keeps
|
|
379
|
+
// running where it is, and because it is now terminal it is forced to push a
|
|
380
|
+
// final up the spine when it finishes. `D` ALSO detaches it to the background
|
|
381
|
+
// `crtr` session (frees the pane; the pi keeps generating). Neither ends it.
|
|
382
|
+
{ name: 'demote to terminal', key: 'd', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' >/dev/null 2>&1"` },
|
|
383
|
+
{ name: 'detach to background', key: 'D', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' --detach >/dev/null 2>&1"` },
|
|
384
|
+
// Close cascades down the subscribes_to spine (kills the subtree's windows,
|
|
385
|
+
// marks them canceled); revivable. Output discarded — the keypress just acts.
|
|
386
|
+
{ name: 'close agent + subtree', key: 'x', cmd: `run-shell "crtr node close --pane '#{pane_id}' >/dev/null 2>&1"` },
|
|
387
|
+
// Re-keyed g→b so `g` is free for the canvas-nav GRAPH toggle (below).
|
|
388
|
+
{ name: 'browse background agents', key: 'b', cmd: `switch-client -t ${sess}` },
|
|
235
389
|
];
|
|
390
|
+
// Canvas-nav chords from config (default: g→graph, m→manager, e→expand). The
|
|
391
|
+
// `__graph__` sentinel toggles the in-pi GRAPH modal via send-keys; every
|
|
392
|
+
// other bind shells the chord dispatcher, which resolves the pane's node and
|
|
393
|
+
// interpolates the bind at popup time. Keys colliding with the built-ins are
|
|
394
|
+
// skipped (the built-in wins).
|
|
395
|
+
let prefixBinds = {};
|
|
396
|
+
try {
|
|
397
|
+
prefixBinds = readConfig('user').canvasNav.prefixBinds;
|
|
398
|
+
}
|
|
399
|
+
catch { /* defaults below */ }
|
|
400
|
+
for (const [key, bind] of Object.entries(prefixBinds)) {
|
|
401
|
+
if (key.length !== 1 || RESERVED_MENU_KEYS.has(key))
|
|
402
|
+
continue;
|
|
403
|
+
const name = bind.desc !== undefined && bind.desc !== '' ? bind.desc : `chord ${key}`;
|
|
404
|
+
const cmd = bind.run === '__graph__'
|
|
405
|
+
? `send-keys -t '#{pane_id}' '/graph' Enter`
|
|
406
|
+
: `run-shell "crtr canvas chord --pane '#{pane_id}' --key ${key} >/dev/null 2>&1"`;
|
|
407
|
+
items.push({ name, key, cmd });
|
|
408
|
+
}
|
|
409
|
+
// Focus report N: nine generated chord items (1..9), each resolved by the
|
|
410
|
+
// dispatcher to subscriptionsOf(self)[N-1] at popup time.
|
|
411
|
+
for (let n = 1; n <= 9; n++) {
|
|
412
|
+
items.push({
|
|
413
|
+
name: `focus report ${n}`,
|
|
414
|
+
key: `${n}`,
|
|
415
|
+
cmd: `run-shell "crtr canvas chord --pane '#{pane_id}' --key ${n} >/dev/null 2>&1"`,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
236
418
|
// tmux's -x sets the menu's LEFT edge. To sit the box INSIDE the pane's
|
|
237
419
|
// top-right corner, shift x left by the box width (longest line + tmux chrome:
|
|
238
420
|
// borders + padding + the right-aligned mnemonic-key column) via format math.
|
package/dist/core/spawn.js
CHANGED
|
@@ -78,6 +78,21 @@ export function spawnAndDetach(opts) {
|
|
|
78
78
|
return { status: 'spawn-failed', message: msg };
|
|
79
79
|
}
|
|
80
80
|
const paneId = split.stdout.trim();
|
|
81
|
+
// Force `remain-on-exit off` at PANE scope on the new pane. remain-on-exit is
|
|
82
|
+
// a pane option (tmux 3.x) inherited from the window-scoped value, and the
|
|
83
|
+
// canvas runtime arms `remain-on-exit on` on a node's vehicle/focus WINDOW
|
|
84
|
+
// (F3 freeze, see runtime/tmux.ts setRemainOnExit). A split-window pane opened
|
|
85
|
+
// into that window inherits the `on`, so the humanloop TUI pane would linger
|
|
86
|
+
// as a dead pane ("pane is dead (status 0, …)") when `crtr human _run` exits 0
|
|
87
|
+
// instead of closing. Overriding at pane scope destroys this pane on clean
|
|
88
|
+
// exit WITHOUT touching the window's value (focus freeze still works) or the
|
|
89
|
+
// user's global config. Best-effort: harmless no-op on tmux where the option
|
|
90
|
+
// is window-only.
|
|
91
|
+
if (paneId !== '') {
|
|
92
|
+
spawnSync('tmux', ['set-option', '-p', '-t', paneId, 'remain-on-exit', 'off'], {
|
|
93
|
+
stdio: 'ignore',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
81
96
|
// Schedule self-kill of the originating pane.
|
|
82
97
|
scheduleKillCurrentPane(opts.killAfterSeconds);
|
|
83
98
|
return {
|
package/dist/daemon/crtrd.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
export type LivenessVerdict = 'leave' | 'pending' | 'revive';
|
|
2
|
+
/** Decide what to do with a node whose tmux pane is alive, from its pi
|
|
3
|
+
* liveness and how long it's been dead. Pure — the time-and-tmux side effects
|
|
4
|
+
* live in handleLiveWindow; this is the unit-testable core.
|
|
5
|
+
* piPidAlive: true=alive, false=dead, null=no pid recorded (legacy node, or a
|
|
6
|
+
* relaunch in flight) — leave those to the pane-gone pass.
|
|
7
|
+
* deadFor: ms since first observed dead, or null on the first observation. */
|
|
8
|
+
export declare function livenessVerdict(piPidAlive: boolean | null, deadFor: number | null): LivenessVerdict;
|
|
1
9
|
/** Read the pid stored in the pidfile, or null if absent / malformed. */
|
|
2
10
|
export declare function readPidfile(): number | null;
|
|
3
|
-
/** True if a process with `pid` is currently alive (signal-0 probe).
|
|
11
|
+
/** True if a process with `pid` is currently alive (signal-0 probe). `kill(pid,
|
|
12
|
+
* 0)` throws ESRCH when the process is gone; EPERM means it exists but isn't
|
|
13
|
+
* ours — still alive. */
|
|
4
14
|
export declare function isPidAlive(pid: number): boolean;
|
|
5
15
|
/** True when a crtrd process is already running (pidfile exists + pid alive). */
|
|
6
16
|
export declare function isDaemonRunning(): boolean;
|
|
17
|
+
export declare function superviseTick(now?: number): Promise<void>;
|
|
7
18
|
export interface DaemonOpts {
|
|
8
19
|
/** Milliseconds between supervision polls. Default 2000. */
|
|
9
20
|
intervalMs?: number;
|