@crouton-kit/crouter 0.3.13 → 0.3.15
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/__tests__/human.test.js +73 -2
- 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.d.ts +1 -0
- package/dist/commands/human/queue.js +105 -2
- package/dist/commands/human/shared.d.ts +28 -18
- package/dist/commands/human/shared.js +53 -60
- package/dist/commands/human.js +6 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +381 -87
- 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 +130 -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 +259 -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.d.ts +1 -0
- package/dist/core/__tests__/passive-subscription.test.js +164 -0
- 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 +244 -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 +183 -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 +328 -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.d.ts +1 -0
- package/dist/core/__tests__/subcommand-tier.test.js +99 -0
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +106 -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 +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -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/paths.d.ts +4 -0
- package/dist/core/canvas/paths.js +6 -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 +48 -7
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +14 -12
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.d.ts +17 -0
- package/dist/core/feed/passive.js +92 -0
- package/dist/core/help.d.ts +59 -13
- package/dist/core/help.js +73 -28
- 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.d.ts +14 -0
- package/dist/core/runtime/demote.js +120 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +32 -6
- package/dist/core/runtime/kickoff.js +111 -37
- package/dist/core/runtime/launch.d.ts +29 -6
- package/dist/core/runtime/launch.js +85 -13
- 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 +32 -1
- package/dist/core/runtime/nodes.js +60 -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 +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -32
- package/dist/core/runtime/presence.js +90 -110
- package/dist/core/runtime/promote.d.ts +18 -7
- package/dist/core/runtime/promote.js +70 -65
- 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/roadmap.d.ts +5 -4
- package/dist/core/runtime/roadmap.js +9 -16
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +169 -44
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux.d.ts +106 -21
- package/dist/core/runtime/tmux.js +249 -45
- 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.d.ts +34 -0
- package/dist/pi-extensions/canvas-commands.js +103 -0
- 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 +21 -0
- package/dist/pi-extensions/canvas-goal-capture.js +67 -0
- 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 +586 -262
- package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
- package/dist/pi-extensions/canvas-passive-context.js +114 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
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';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Shell quoting + tmux invocation
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
@@ -69,7 +70,8 @@ function envFlags(env) {
|
|
|
69
70
|
}
|
|
70
71
|
/** Open a background window for a node and run `command` in it. `-d` keeps it
|
|
71
72
|
* detached so it doesn't steal focus or become the current window. Returns the
|
|
72
|
-
* new window id
|
|
73
|
+
* new window id AND the pane id it created (the durable `%pane_id`, LOCATION's
|
|
74
|
+
* anchor) — callers that only need the window destructure `.window`.
|
|
73
75
|
*
|
|
74
76
|
* Target is `${session}:` (trailing colon = the session, no window index) plus
|
|
75
77
|
* `-a` (insert after the current window) so tmux allocates the next free index.
|
|
@@ -78,7 +80,8 @@ function envFlags(env) {
|
|
|
78
80
|
* "create window failed: index N in use" whenever the active window is not the
|
|
79
81
|
* last one (common when base-index is 0 but the live window sits at index 1).
|
|
80
82
|
* `-a` also keeps node windows off index 0, which is reserved for the optional
|
|
81
|
-
* dashboard.
|
|
83
|
+
* dashboard. The explicit `-t ${session}:` target is the §2.2 HARD DRIVER
|
|
84
|
+
* INVARIANT — never let new-window fall back to tmux's global current session. */
|
|
82
85
|
export function openNodeWindow(opts) {
|
|
83
86
|
const r = tmux([
|
|
84
87
|
'new-window',
|
|
@@ -86,7 +89,7 @@ export function openNodeWindow(opts) {
|
|
|
86
89
|
'-a',
|
|
87
90
|
'-P',
|
|
88
91
|
'-F',
|
|
89
|
-
'#{window_id}',
|
|
92
|
+
'#{window_id}\t#{pane_id}',
|
|
90
93
|
'-t',
|
|
91
94
|
`${opts.session}:`,
|
|
92
95
|
'-n',
|
|
@@ -96,27 +99,41 @@ export function openNodeWindow(opts) {
|
|
|
96
99
|
...envFlags(opts.env),
|
|
97
100
|
opts.command,
|
|
98
101
|
]);
|
|
99
|
-
return r.ok ? r.stdout : null;
|
|
100
|
-
}
|
|
101
|
-
/** Open a background window running a plain login shell (no pi) and return its
|
|
102
|
-
* window + pane ids. Used by demote: the agent's pi is swapped OUT into this
|
|
103
|
-
* window's slot and the shell is swapped INTO the caller's pane. `-a` keeps it
|
|
104
|
-
* off index 0 (reserved for a dashboard), `-d` keeps it from stealing focus. */
|
|
105
|
-
export function openShellWindow(opts) {
|
|
106
|
-
const r = tmux([
|
|
107
|
-
'new-window', '-d', '-a', '-P',
|
|
108
|
-
'-F', '#{window_id}\t#{pane_id}',
|
|
109
|
-
'-t', `${opts.session}:`,
|
|
110
|
-
'-n', opts.name,
|
|
111
|
-
'-c', opts.cwd,
|
|
112
|
-
]);
|
|
113
102
|
if (!r.ok)
|
|
114
103
|
return null;
|
|
115
104
|
const [window, pane] = r.stdout.split('\t');
|
|
116
|
-
if (window === undefined || pane === undefined)
|
|
105
|
+
if (window === undefined || window === '' || pane === undefined || pane === '') {
|
|
117
106
|
return null;
|
|
107
|
+
}
|
|
118
108
|
return { window, pane };
|
|
119
109
|
}
|
|
110
|
+
/** Split `targetPane`'s window, opening a NEW pane beside it running `command`,
|
|
111
|
+
* and return the new pane id (the durable `%id`). The ONLY new-pane-beside verb
|
|
112
|
+
* (Q3: a focus opened side-by-side). `-d` keeps the caller's pane active; `-h`
|
|
113
|
+
* makes the split side-by-side (left/right), the default for a focus viewport.
|
|
114
|
+
*
|
|
115
|
+
* §2.2 HARD DRIVER INVARIANT: `targetPane` is REQUIRED — a bare `split-window`
|
|
116
|
+
* would split tmux's global current pane, which can leak a pane into an
|
|
117
|
+
* unrelated user session (the exact bug this design kills). The explicit
|
|
118
|
+
* `-t <targetPane>` makes the destination structurally un-leakable. Returns
|
|
119
|
+
* null if tmux fails. */
|
|
120
|
+
export function splitWindow(targetPane, opts) {
|
|
121
|
+
const r = tmux([
|
|
122
|
+
'split-window',
|
|
123
|
+
'-d',
|
|
124
|
+
...(opts.vertical === true ? [] : ['-h']),
|
|
125
|
+
'-P',
|
|
126
|
+
'-F',
|
|
127
|
+
'#{pane_id}',
|
|
128
|
+
'-t',
|
|
129
|
+
targetPane,
|
|
130
|
+
'-c',
|
|
131
|
+
opts.cwd,
|
|
132
|
+
...envFlags(opts.env),
|
|
133
|
+
opts.command,
|
|
134
|
+
]);
|
|
135
|
+
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
136
|
+
}
|
|
120
137
|
/** Bring a node's window forefront. Switches client across roots when needed. */
|
|
121
138
|
export function focusWindow(session, window) {
|
|
122
139
|
const here = currentTmux();
|
|
@@ -131,6 +148,14 @@ export function focusWindow(session, window) {
|
|
|
131
148
|
export function closeWindow(window) {
|
|
132
149
|
return tmux(['kill-window', '-t', window]).ok;
|
|
133
150
|
}
|
|
151
|
+
/** Close a single PANE. Its window closes automatically once this was the last
|
|
152
|
+
* pane, but sibling panes survive — so co-located nodes (several agents sharing
|
|
153
|
+
* one window via swap-pane focus) are torn down one at a time instead of all
|
|
154
|
+
* at once by a window kill. Pane ids are the stable vehicle handle; windows
|
|
155
|
+
* shift under swap-pane focus, so pane-granular teardown is the correct unit. */
|
|
156
|
+
export function closePane(pane) {
|
|
157
|
+
return tmux(['kill-pane', '-t', pane]).ok;
|
|
158
|
+
}
|
|
134
159
|
/** The active pane id of a window. Node windows are single-pane, so this is the
|
|
135
160
|
* node's pane. Returns null if the window is gone or tmux fails. */
|
|
136
161
|
export function paneOfWindow(session, window) {
|
|
@@ -145,6 +170,45 @@ export function windowOfPane(pane) {
|
|
|
145
170
|
const r = tmux(['display-message', '-p', '-t', pane, '#{window_id}']);
|
|
146
171
|
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
147
172
|
}
|
|
173
|
+
/** The session + window a pane currently lives in (`display-message -p -t %id`).
|
|
174
|
+
* The §2.4 reconciliation read-back: resolve a node's/focus's CURRENT
|
|
175
|
+
* window/session from its durable pane id before any act, so crtr follows a
|
|
176
|
+
* manual `move-pane`/`join-pane`/`break-pane` instead of fighting it. Null if
|
|
177
|
+
* the pane is gone or tmux fails. */
|
|
178
|
+
export function paneLocation(pane) {
|
|
179
|
+
const r = tmux(['display-message', '-p', '-t', pane, '#{session_name}\t#{window_id}']);
|
|
180
|
+
if (!r.ok)
|
|
181
|
+
return null;
|
|
182
|
+
const [session, window] = r.stdout.split('\t');
|
|
183
|
+
if (session === undefined || session === '' || window === undefined || window === '')
|
|
184
|
+
return null;
|
|
185
|
+
return { session, window };
|
|
186
|
+
}
|
|
187
|
+
/** Does this pane id still exist? A `display-message` probe on the `%id` — the
|
|
188
|
+
* v3 PRIMARY liveness probe (§1.2/§2.2), replacing window-existence so a user
|
|
189
|
+
* moving a pane to another window/session never reads as "gone". True iff tmux
|
|
190
|
+
* knows the pane.
|
|
191
|
+
*
|
|
192
|
+
* NOTE: `display-message -p -t <gone-pane>` EXITS 0 with EMPTY output (it does
|
|
193
|
+
* not error on an unresolvable pane target) — so an `.ok` check alone would
|
|
194
|
+
* report a dead pane as alive, defeating the whole point of pane-existence
|
|
195
|
+
* liveness. We therefore require the echoed `#{pane_id}` to equal the requested
|
|
196
|
+
* pane: a live pane echoes its own id, a gone/bogus one yields empty. */
|
|
197
|
+
export function paneExists(pane) {
|
|
198
|
+
const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}']);
|
|
199
|
+
return r.ok && r.stdout === pane;
|
|
200
|
+
}
|
|
201
|
+
/** Relocate a pane into another session as its own window WITHOUT killing the
|
|
202
|
+
* process in it — `break-pane -d` moves the pane out of its current window (the
|
|
203
|
+
* pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
|
|
204
|
+
* client where it is rather than following the pane to the background, and `-a`
|
|
205
|
+
* allocates the next free window index (same dodge as openNodeWindow). The
|
|
206
|
+
* "detach to background" driver behind `node lifecycle --detach`. Best-effort;
|
|
207
|
+
* false if tmux refuses (e.g. the pane is gone). The caller reconciles presence
|
|
208
|
+
* so the canvas follows the move. */
|
|
209
|
+
export function breakPaneToSession(pane, session) {
|
|
210
|
+
return tmux(['break-pane', '-d', '-a', '-s', pane, '-t', `${session}:`]).ok;
|
|
211
|
+
}
|
|
148
212
|
/** Swap `targetPane` into `callerPane`'s layout slot, IN PLACE. `-d` keeps the
|
|
149
213
|
* caller's window active, so the target's pane appears where the caller is
|
|
150
214
|
* rather than navigating the client off to the target's window. The caller's
|
|
@@ -155,26 +219,34 @@ export function swapPaneInPlace(targetPane, callerPane) {
|
|
|
155
219
|
return true;
|
|
156
220
|
return tmux(['swap-pane', '-d', '-s', targetPane, '-t', callerPane]).ok;
|
|
157
221
|
}
|
|
158
|
-
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
222
|
+
/** The `respawn-pane -k` argv for `opts`. `-k` kills the pane's current process
|
|
223
|
+
* (e.g. a yielding pi) and re-execs `command` in the SAME pane, preserving its
|
|
224
|
+
* `%id` (§1.5 F3: a frozen focus pane resumes in place, no new window). The
|
|
225
|
+
* explicit `-t opts.pane` is the §2.2 HARD DRIVER INVARIANT — respawn must name
|
|
226
|
+
* its target pane, never tmux's global current pane. */
|
|
227
|
+
function respawnPaneArgs(opts) {
|
|
228
|
+
return [
|
|
229
|
+
'respawn-pane',
|
|
230
|
+
'-k',
|
|
231
|
+
'-c',
|
|
232
|
+
opts.cwd,
|
|
233
|
+
...envFlags(opts.env),
|
|
234
|
+
'-t',
|
|
235
|
+
opts.pane,
|
|
236
|
+
opts.command,
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
/** Re-exec a command in an EXISTING pane, in place — DETACHED. Spawned in its own
|
|
240
|
+
* process group (unref'd) so the request reaches the tmux server even though
|
|
241
|
+
* `-k` tears down the caller's own pi mid-flight. Used when a node respawns ITS
|
|
242
|
+
* OWN pane (refresh-yield): the dispatch can't be awaited because it kills the
|
|
243
|
+
* awaiter. Returns true once the request was dispatched. */
|
|
244
|
+
export function respawnPaneDetached(opts) {
|
|
167
245
|
try {
|
|
168
|
-
const child = spawn('tmux',
|
|
169
|
-
|
|
170
|
-
'
|
|
171
|
-
|
|
172
|
-
opts.cwd,
|
|
173
|
-
...envFlags(opts.env),
|
|
174
|
-
'-t',
|
|
175
|
-
opts.pane,
|
|
176
|
-
opts.command,
|
|
177
|
-
], { detached: true, stdio: 'ignore' });
|
|
246
|
+
const child = spawn('tmux', respawnPaneArgs(opts), {
|
|
247
|
+
detached: true,
|
|
248
|
+
stdio: 'ignore',
|
|
249
|
+
});
|
|
178
250
|
child.unref();
|
|
179
251
|
return true;
|
|
180
252
|
}
|
|
@@ -182,6 +254,19 @@ export function respawnPane(opts) {
|
|
|
182
254
|
return false;
|
|
183
255
|
}
|
|
184
256
|
}
|
|
257
|
+
/** Re-exec a command in an EXISTING pane, in place — SYNCHRONOUS. Runs the
|
|
258
|
+
* `respawn-pane` to completion and reports the real exit status. Used when the
|
|
259
|
+
* caller is NOT the pane being respawned (e.g. the daemon resuming a frozen
|
|
260
|
+
* focus pane), so it can confirm the respawn landed. Returns true on success. */
|
|
261
|
+
export function respawnPaneSync(opts) {
|
|
262
|
+
return tmux(respawnPaneArgs(opts)).ok;
|
|
263
|
+
}
|
|
264
|
+
/** @deprecated Use respawnPaneDetached. Retained so existing refresh-yield
|
|
265
|
+
* callers stay green while the placement layer migrates onto the explicit
|
|
266
|
+
* sync/detached split. */
|
|
267
|
+
export function respawnPane(opts) {
|
|
268
|
+
return respawnPaneDetached(opts);
|
|
269
|
+
}
|
|
185
270
|
// ---------------------------------------------------------------------------
|
|
186
271
|
// pi command assembly
|
|
187
272
|
// ---------------------------------------------------------------------------
|
|
@@ -224,21 +309,140 @@ export function switchClient(session) {
|
|
|
224
309
|
return tmux(['switch-client', '-t', session]).ok;
|
|
225
310
|
}
|
|
226
311
|
// ---------------------------------------------------------------------------
|
|
312
|
+
// Multi-pane layout (used by `canvas tmux-spread`)
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
/** Move a source pane into a destination window (`tmux join-pane`). The source
|
|
315
|
+
* pane's running process (e.g. a child's live pi) is preserved; its now-empty
|
|
316
|
+
* source window auto-closes. Best-effort; false if tmux fails. */
|
|
317
|
+
export function joinPane(srcPane, dstWindow) {
|
|
318
|
+
return tmux(['join-pane', '-s', srcPane, '-t', dstWindow]).ok;
|
|
319
|
+
}
|
|
320
|
+
/** Apply a named tmux layout to a window (`tmux select-layout`). Use
|
|
321
|
+
* `main-vertical` for one wide pane on the left + the rest stacked right.
|
|
322
|
+
* Best-effort; never throws. */
|
|
323
|
+
export function selectLayout(window, layout) {
|
|
324
|
+
return tmux(['select-layout', '-t', window, layout]).ok;
|
|
325
|
+
}
|
|
326
|
+
/** Set a tmux window option (`tmux set-window-option`). Used to size the main
|
|
327
|
+
* pane (`main-pane-width`) before a main-vertical layout. Best-effort. */
|
|
328
|
+
export function setWindowOption(window, name, value) {
|
|
329
|
+
return tmux(['set-window-option', '-t', window, name, value]).ok;
|
|
330
|
+
}
|
|
331
|
+
/** Toggle `remain-on-exit` on a window (§1.5 F3). `on` keeps a focus pane on
|
|
332
|
+
* screen after its pi exits — the viewport survives (F1), the final transcript
|
|
333
|
+
* is preserved, and `respawn-pane -k` can resurrect the node into the SAME pane
|
|
334
|
+
* id. NOTE (§1.5/§2.5, spike-confirmed): a dead/frozen pane is reaped only by
|
|
335
|
+
* `kill-pane`/`respawn-pane`, NEVER by toggling this off — the toggle does not
|
|
336
|
+
* reap an already-dead pane. Best-effort; never throws. */
|
|
337
|
+
export function setRemainOnExit(window, on) {
|
|
338
|
+
return tmux(['set-window-option', '-t', window, 'remain-on-exit', on ? 'on' : 'off']).ok;
|
|
339
|
+
}
|
|
340
|
+
/** Type a literal (e.g. a `/graph` slash command) into a pane and press Enter
|
|
341
|
+
* (`tmux send-keys -t <pane> '<text>' Enter`). Requires the pane's editor be
|
|
342
|
+
* empty, same limitation as the menu's `/promote` item. Best-effort. */
|
|
343
|
+
export function sendKeysEnter(pane, text) {
|
|
344
|
+
return tmux(['send-keys', '-t', pane, text, 'Enter']).ok;
|
|
345
|
+
}
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
227
347
|
// Prefix menu — Alt+C opens a which-key-style tmux display-menu of crouter
|
|
228
348
|
// actions. Installed on the running server at root boot; idempotent (a re-bind
|
|
229
349
|
// overwrites the previous one). Items shell out to `crtr`, passing the active
|
|
230
350
|
// pane so an action targets the agent currently in front of you.
|
|
231
351
|
// ---------------------------------------------------------------------------
|
|
232
|
-
/**
|
|
352
|
+
/** Reserved mnemonic keys owned by the built-in menu items below — a custom
|
|
353
|
+
* `prefixBind` may not claim these (the built-in item wins). */
|
|
354
|
+
const RESERVED_MENU_KEYS = new Set(['o', 'd', 'D', 'x', 'b']);
|
|
355
|
+
/** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
|
|
356
|
+
* The built-in items (promote/demote/detach/close/browse) are static; the canvas-nav
|
|
357
|
+
* chords (graph/manager/expand/report-N + any custom prefixBind) are appended
|
|
358
|
+
* from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
|
|
359
|
+
* the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
|
|
360
|
+
* while behaviour is config-driven. */
|
|
233
361
|
export function installMenuBinding() {
|
|
234
362
|
const sess = nodeSession();
|
|
235
|
-
|
|
363
|
+
const title = ' crtr ';
|
|
364
|
+
const items = [
|
|
365
|
+
// Promote types `/promote` into the agent's pane rather than shelling out:
|
|
366
|
+
// the slash command delivers the orchestration guidance into the node's
|
|
367
|
+
// context, which a bare `run-shell` (output discarded) could not.
|
|
368
|
+
{ name: 'promote to orchestrator', key: 'o', cmd: `send-keys -t '#{pane_id}' '/promote' Enter` },
|
|
369
|
+
// `d` demotes the agent to TERMINAL in place: no finalize, no kill — it keeps
|
|
370
|
+
// running where it is, and because it is now terminal it is forced to push a
|
|
371
|
+
// final up the spine when it finishes. `D` ALSO detaches it to the background
|
|
372
|
+
// `crtr` session (frees the pane; the pi keeps generating). Neither ends it.
|
|
373
|
+
{ name: 'demote to terminal', key: 'd', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' >/dev/null 2>&1"` },
|
|
374
|
+
{ name: 'detach to background', key: 'D', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' --detach >/dev/null 2>&1"` },
|
|
375
|
+
// Close cascades down the subscribes_to spine (kills the subtree's windows,
|
|
376
|
+
// marks them canceled); revivable. Output discarded — the keypress just acts.
|
|
377
|
+
{ name: 'close agent + subtree', key: 'x', cmd: `run-shell "crtr node close --pane '#{pane_id}' >/dev/null 2>&1"` },
|
|
378
|
+
// Re-keyed g→b so `g` is free for the canvas-nav GRAPH toggle (below).
|
|
379
|
+
{ name: 'browse background agents', key: 'b', cmd: `switch-client -t ${sess}` },
|
|
380
|
+
];
|
|
381
|
+
// Canvas-nav chords from config (default: g→graph, m→manager, e→expand). The
|
|
382
|
+
// `__graph__` sentinel toggles the in-pi GRAPH modal via send-keys; every
|
|
383
|
+
// other bind shells the chord dispatcher, which resolves the pane's node and
|
|
384
|
+
// interpolates the bind at popup time. Keys colliding with the built-ins are
|
|
385
|
+
// skipped (the built-in wins).
|
|
386
|
+
let prefixBinds = {};
|
|
387
|
+
try {
|
|
388
|
+
prefixBinds = readConfig('user').canvasNav.prefixBinds;
|
|
389
|
+
}
|
|
390
|
+
catch { /* defaults below */ }
|
|
391
|
+
for (const [key, bind] of Object.entries(prefixBinds)) {
|
|
392
|
+
if (key.length !== 1 || RESERVED_MENU_KEYS.has(key))
|
|
393
|
+
continue;
|
|
394
|
+
const name = bind.desc !== undefined && bind.desc !== '' ? bind.desc : `chord ${key}`;
|
|
395
|
+
const cmd = bind.run === '__graph__'
|
|
396
|
+
? `send-keys -t '#{pane_id}' '/graph' Enter`
|
|
397
|
+
: `run-shell "crtr canvas chord --pane '#{pane_id}' --key ${key} >/dev/null 2>&1"`;
|
|
398
|
+
items.push({ name, key, cmd });
|
|
399
|
+
}
|
|
400
|
+
// Focus report N: nine generated chord items (1..9), each resolved by the
|
|
401
|
+
// dispatcher to subscriptionsOf(self)[N-1] at popup time.
|
|
402
|
+
for (let n = 1; n <= 9; n++) {
|
|
403
|
+
items.push({
|
|
404
|
+
name: `focus report ${n}`,
|
|
405
|
+
key: `${n}`,
|
|
406
|
+
cmd: `run-shell "crtr canvas chord --pane '#{pane_id}' --key ${n} >/dev/null 2>&1"`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
// tmux's -x sets the menu's LEFT edge. To sit the box INSIDE the pane's
|
|
410
|
+
// top-right corner, shift x left by the box width (longest line + tmux chrome:
|
|
411
|
+
// borders + padding + the right-aligned mnemonic-key column) via format math.
|
|
412
|
+
const boxW = Math.max(title.length, ...items.map((i) => i.name.length)) + 6;
|
|
413
|
+
// Fine-tune nudges off the pane's top-right corner: a hair further left and
|
|
414
|
+
// one row down so the box doesn't kiss the pane border.
|
|
415
|
+
const nudgeX = 1; // extra columns left
|
|
416
|
+
const nudgeY = 3; // rows down
|
|
417
|
+
const args = [
|
|
236
418
|
'bind-key', '-n', 'M-c', 'display-menu',
|
|
237
|
-
'-T',
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
419
|
+
'-T', `#[align=centre]${title}`,
|
|
420
|
+
'-x', `#{e|-:#{pane_right},${boxW + nudgeX}}`,
|
|
421
|
+
'-y', `#{e|+:#{pane_top},${nudgeY}}`,
|
|
422
|
+
];
|
|
423
|
+
for (const it of items)
|
|
424
|
+
args.push(it.name, it.key, it.cmd);
|
|
425
|
+
return tmux(args).ok;
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Nav bindings — Alt+] / Alt+[ DFS-walk the canvas one window at a time. Each
|
|
429
|
+
// key shells out to `crtr node cycle`, passing the active pane so the walk is
|
|
430
|
+
// relative to the agent in front of you; cycle then swaps the next/prev node
|
|
431
|
+
// into that pane (like `node focus`). Output is discarded so the keypress never
|
|
432
|
+
// pops a results view. Installed at root boot alongside the Alt+C menu.
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
/** Bind Alt+] (forward) and Alt+[ (back) to the DFS canvas walk. Best-effort;
|
|
435
|
+
* false if either bind fails. NOTE: Alt+[ is only delivered cleanly when the
|
|
436
|
+
* terminal/tmux disambiguate it from a raw CSI introducer (`extended-keys on`).
|
|
437
|
+
*/
|
|
438
|
+
export function installNavBindings() {
|
|
439
|
+
const next = tmux([
|
|
440
|
+
'bind-key', '-n', 'M-]', 'run-shell',
|
|
441
|
+
`crtr node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`,
|
|
442
|
+
]).ok;
|
|
443
|
+
const prev = tmux([
|
|
444
|
+
'bind-key', '-n', 'M-[', 'run-shell',
|
|
445
|
+
`crtr node cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`,
|
|
243
446
|
]).ok;
|
|
447
|
+
return next && prev;
|
|
244
448
|
}
|
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;
|