@crouton-kit/crouter 0.3.18 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/PERSONA.md +1 -0
- package/dist/builtin-personas/design/orchestrator.md +2 -1
- package/dist/builtin-personas/developer/PERSONA.md +1 -0
- package/dist/builtin-personas/developer/orchestrator.md +2 -1
- package/dist/builtin-personas/explore/PERSONA.md +1 -0
- package/dist/builtin-personas/explore/orchestrator.md +4 -0
- package/dist/builtin-personas/general/PERSONA.md +1 -0
- package/dist/builtin-personas/general/orchestrator.md +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/orchestrator.md +2 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
- package/dist/builtin-personas/review/PERSONA.md +1 -0
- package/dist/builtin-personas/review/orchestrator.md +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/PERSONA.md +1 -0
- package/dist/builtin-personas/spec/orchestrator.md +2 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +3 -2
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/queue.js +54 -8
- package/dist/commands/human/shared.d.ts +2 -0
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +610 -61
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
- package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
- package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
- package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
- package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
- package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
- package/dist/core/__tests__/broker-dialogs.test.js +126 -0
- package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
- package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
- package/dist/core/__tests__/helpers/broker-clients.js +178 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
- package/dist/core/__tests__/live-mutation.test.js +6 -132
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +16 -23
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +91 -3
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/loader.d.ts +7 -0
- package/dist/core/personas/loader.js +36 -5
- package/dist/core/personas/resolve.js +13 -3
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +54 -4
- package/dist/core/runtime/launch.js +85 -5
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +37 -0
- package/dist/core/runtime/nodes.js +60 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +32 -14
- package/dist/core/runtime/placement.js +206 -52
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +4 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +11 -8
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +23 -0
- package/dist/core/runtime/spawn.js +77 -23
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +5 -0
- package/dist/core/spawn.js +20 -2
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +87 -23
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
|
|
3
|
+
* `BUILTIN_SLASH_COMMANDS` (review C1). **21 entries at 0.78.1** (review n1 — NOT 23).
|
|
4
|
+
*
|
|
5
|
+
* Builtins are NOT engine-interpreted (`session.prompt('/model')` ships `/model`
|
|
6
|
+
* to the LLM as literal text), so the broker's `get_commands` op MERGES this list
|
|
7
|
+
* with the engine's registered commands/templates/skills, and the viewer
|
|
8
|
+
* autocomplete parses these locally. Both the broker (T3) and the viewer (T6)
|
|
9
|
+
* import this single copy.
|
|
10
|
+
*
|
|
11
|
+
* Upstream interpolates `${APP_NAME}` into the `quit` description; `APP_NAME`
|
|
12
|
+
* defaults to "pi" (overridable via pi config), so the vendored copy hardcodes
|
|
13
|
+
* "Quit pi".
|
|
14
|
+
*/
|
|
15
|
+
export declare const BUILTIN_SLASH_COMMANDS: ReadonlyArray<{
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// pi-vendored.ts — symbols copied verbatim from pi that pi does NOT re-export.
|
|
2
|
+
//
|
|
3
|
+
// VENDORED, not imported, because pi's `exports` map is `.`-only (+`./hooks`):
|
|
4
|
+
// a deep import like `@earendil-works/pi-coding-agent/dist/core/slash-commands`
|
|
5
|
+
// fails with ERR_PACKAGE_PATH_NOT_EXPORTED, and the symbol is absent from the
|
|
6
|
+
// package-root `index.d.ts` at BOTH 0.78.1 and 0.79.0 — so there is no pin at
|
|
7
|
+
// which it becomes importable (review C1). The only safe access is to copy it.
|
|
8
|
+
//
|
|
9
|
+
// MAINTENANCE: re-sync on EVERY pi SDK bump. The source of truth is
|
|
10
|
+
// `node_modules/@earendil-works/pi-coding-agent/dist/core/slash-commands.js`.
|
|
11
|
+
// If the upstream list changes (entries added/removed/reworded), update the copy
|
|
12
|
+
// below to match and adjust the count note.
|
|
13
|
+
/**
|
|
14
|
+
* pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
|
|
15
|
+
* `BUILTIN_SLASH_COMMANDS` (review C1). **21 entries at 0.78.1** (review n1 — NOT 23).
|
|
16
|
+
*
|
|
17
|
+
* Builtins are NOT engine-interpreted (`session.prompt('/model')` ships `/model`
|
|
18
|
+
* to the LLM as literal text), so the broker's `get_commands` op MERGES this list
|
|
19
|
+
* with the engine's registered commands/templates/skills, and the viewer
|
|
20
|
+
* autocomplete parses these locally. Both the broker (T3) and the viewer (T6)
|
|
21
|
+
* import this single copy.
|
|
22
|
+
*
|
|
23
|
+
* Upstream interpolates `${APP_NAME}` into the `quit` description; `APP_NAME`
|
|
24
|
+
* defaults to "pi" (overridable via pi config), so the vendored copy hardcodes
|
|
25
|
+
* "Quit pi".
|
|
26
|
+
*/
|
|
27
|
+
export const BUILTIN_SLASH_COMMANDS = [
|
|
28
|
+
{ name: 'settings', description: 'Open settings menu' },
|
|
29
|
+
{ name: 'model', description: 'Select model (opens selector UI)' },
|
|
30
|
+
{ name: 'scoped-models', description: 'Enable/disable models for Ctrl+P cycling' },
|
|
31
|
+
{ name: 'export', description: 'Export session (HTML default, or specify path: .html/.jsonl)' },
|
|
32
|
+
{ name: 'import', description: 'Import and resume a session from a JSONL file' },
|
|
33
|
+
{ name: 'share', description: 'Share session as a secret GitHub gist' },
|
|
34
|
+
{ name: 'copy', description: 'Copy last agent message to clipboard' },
|
|
35
|
+
{ name: 'name', description: 'Set session display name' },
|
|
36
|
+
{ name: 'session', description: 'Show session info and stats' },
|
|
37
|
+
{ name: 'changelog', description: 'Show changelog entries' },
|
|
38
|
+
{ name: 'hotkeys', description: 'Show all keyboard shortcuts' },
|
|
39
|
+
{ name: 'fork', description: 'Create a new fork from a previous user message' },
|
|
40
|
+
{ name: 'clone', description: 'Duplicate the current session at the current position' },
|
|
41
|
+
{ name: 'tree', description: 'Navigate session tree (switch branches)' },
|
|
42
|
+
{ name: 'login', description: 'Configure provider authentication' },
|
|
43
|
+
{ name: 'logout', description: 'Remove provider authentication' },
|
|
44
|
+
{ name: 'new', description: 'Start a new session' },
|
|
45
|
+
{ name: 'compact', description: 'Manually compact the session context' },
|
|
46
|
+
{ name: 'resume', description: 'Resume a different session' },
|
|
47
|
+
{ name: 'reload', description: 'Reload keybindings, extensions, skills, prompts, and themes' },
|
|
48
|
+
{ name: 'quit', description: 'Quit pi' },
|
|
49
|
+
];
|
|
@@ -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. */
|
|
@@ -155,8 +155,10 @@ export declare function reviveTarget(focus: FocusRow | null, focusPaneAlive: boo
|
|
|
155
155
|
* nothing else. That is the structural bug-kill.
|
|
156
156
|
*
|
|
157
157
|
* `setPresence` (the one atomic LOCATION write) records where the node landed.
|
|
158
|
-
* CRTR_ROOT_SESSION is forced to
|
|
159
|
-
* 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. */
|
|
160
162
|
export declare function reviveIntoPlacement(nodeId: string, launch: ReviveLaunch): PlacementResult;
|
|
161
163
|
/** Relocate a node's still-running agent to the background `crtr` session,
|
|
162
164
|
* freeing the foreground pane WITHOUT killing the pi. `break-pane` moves the
|
|
@@ -166,7 +168,15 @@ export declare function reviveIntoPlacement(nodeId: string, launch: ReviveLaunch
|
|
|
166
168
|
* CURRENT location, §2.4) and again after (presence FOLLOWS the move). No-op
|
|
167
169
|
* (false) when there is no live pane to relocate or tmux refuses the break.
|
|
168
170
|
* `pane` is the authoritative node pane the caller acts on (the Alt+C menu's
|
|
169
|
-
* `#{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). */
|
|
170
180
|
export declare function detachToBackground(nodeId: string, pane?: string): boolean;
|
|
171
181
|
/** A reviver: resume a DORMANT node into its backstage placement (a fresh `crtr`
|
|
172
182
|
* window via reviveIntoPlacement). Injected so placement.ts need not import
|
|
@@ -277,20 +287,28 @@ export declare function recycleFocusPane(nodeId: string, pane: string, launch: R
|
|
|
277
287
|
/** §1.6 lifecycle successor — hand a truly-done focused node's viewport to its
|
|
278
288
|
* manager. Repoints the focus row `focusId` to `managerId` (a DB swap of the
|
|
279
289
|
* occupant). Two takeover realizations, split on the manager's liveness:
|
|
280
|
-
* - DORMANT manager (dead pi)
|
|
281
|
-
* woken by the finished node's `push
|
|
282
|
-
* by the external daemon INTO this
|
|
283
|
-
* (remain-on-exit), where reviveIntoPlacement's
|
|
284
|
-
* 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).
|
|
285
298
|
* - LIVE manager (pi alive in the backstage, the normal multi-child state):
|
|
286
299
|
* the daemon never revives it (it only respawns dead-pi nodes), so we must
|
|
287
300
|
* bring it into the viewport SYNCHRONOUSLY here — swap its backstage pane
|
|
288
301
|
* into the focus slot (MAJOR 1). Otherwise the manager runs off-screen
|
|
289
302
|
* forever while %m sits orphaned in the viewport and the focus row lies
|
|
290
303
|
* about LOCATION.
|
|
291
|
-
* Returns false — the caller closes the focus (Q1
|
|
292
|
-
* the
|
|
293
|
-
*
|
|
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).
|
|
294
312
|
*
|
|
295
313
|
* Why the live swap is NOT the forbidden self-saw: `swap-pane -d` only EXCHANGES
|
|
296
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 { spawnSync } from 'node:child_process';
|
|
29
|
+
import { join } from 'node:path';
|
|
28
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).
|
|
@@ -74,21 +78,6 @@ export function listFocuses() {
|
|
|
74
78
|
// ---------------------------------------------------------------------------
|
|
75
79
|
// Graph → focus routing (for surfacing human-in-the-loop prompts)
|
|
76
80
|
// ---------------------------------------------------------------------------
|
|
77
|
-
/** The root of a node's spine: walk the `parent` column up to `parent == null`.
|
|
78
|
-
* Cycle-guarded (parents must not cycle, but never loop forever). */
|
|
79
|
-
function rootOfSpine(nodeId) {
|
|
80
|
-
let cur = nodeId;
|
|
81
|
-
const seen = new Set();
|
|
82
|
-
for (;;) {
|
|
83
|
-
if (seen.has(cur))
|
|
84
|
-
return cur;
|
|
85
|
-
seen.add(cur);
|
|
86
|
-
const row = getRow(cur);
|
|
87
|
-
if (row === null || row.parent == null)
|
|
88
|
-
return cur;
|
|
89
|
-
cur = row.parent;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
81
|
/** The on-screen viewport a human-in-the-loop prompt raised by `nodeId` should
|
|
93
82
|
* surface into: the HIGHEST FOCUSED node of nodeId's graph — the focused node
|
|
94
83
|
* closest to the graph root, i.e. the session/window the user is actually
|
|
@@ -245,8 +234,10 @@ export function reviveTarget(focus, focusPaneAlive, homeSession) {
|
|
|
245
234
|
* nothing else. That is the structural bug-kill.
|
|
246
235
|
*
|
|
247
236
|
* `setPresence` (the one atomic LOCATION write) records where the node landed.
|
|
248
|
-
* CRTR_ROOT_SESSION is forced to
|
|
249
|
-
* 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. */
|
|
250
241
|
export function reviveIntoPlacement(nodeId, launch) {
|
|
251
242
|
// §2.4 — follow any manual pane move before acting.
|
|
252
243
|
reconcile(nodeId);
|
|
@@ -254,9 +245,12 @@ export function reviveIntoPlacement(nodeId, launch) {
|
|
|
254
245
|
const focusPaneAlive = focus !== null && focus.pane !== null && paneExists(focus.pane);
|
|
255
246
|
const homeSession = homeSessionOf(nodeId);
|
|
256
247
|
const decision = reviveTarget(focus, focusPaneAlive, homeSession);
|
|
257
|
-
// The node's children always spawn into the backstage
|
|
258
|
-
//
|
|
259
|
-
|
|
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) };
|
|
260
254
|
if (decision.kind === 'focus-pane') {
|
|
261
255
|
// F3: resume the pi INTO the live focus pane, in its CURRENT session (Q4 —
|
|
262
256
|
// reconcileFocus follows a user move of the focus pane). No new window.
|
|
@@ -299,7 +293,15 @@ export function reviveIntoPlacement(nodeId, launch) {
|
|
|
299
293
|
* CURRENT location, §2.4) and again after (presence FOLLOWS the move). No-op
|
|
300
294
|
* (false) when there is no live pane to relocate or tmux refuses the break.
|
|
301
295
|
* `pane` is the authoritative node pane the caller acts on (the Alt+C menu's
|
|
302
|
-
* `#{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). */
|
|
303
305
|
export function detachToBackground(nodeId, pane) {
|
|
304
306
|
reconcile(nodeId);
|
|
305
307
|
const row = getRow(nodeId);
|
|
@@ -312,6 +314,28 @@ export function detachToBackground(nodeId, pane) {
|
|
|
312
314
|
// follows the right pane.
|
|
313
315
|
if (row.pane !== target)
|
|
314
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.
|
|
315
339
|
const session = nodeSession();
|
|
316
340
|
ensureSession(session, row.cwd);
|
|
317
341
|
const ok = breakPaneToSession(target, session);
|
|
@@ -340,18 +364,6 @@ function holderId(focusId) {
|
|
|
340
364
|
function newFocusId() {
|
|
341
365
|
return `f-${newNodeId()}`;
|
|
342
366
|
}
|
|
343
|
-
/** signal-0 liveness probe for a pi pid (mirrors the daemon's isPidAlive). */
|
|
344
|
-
function pidAlive(pid) {
|
|
345
|
-
if (pid == null)
|
|
346
|
-
return false;
|
|
347
|
-
try {
|
|
348
|
-
process.kill(pid, 0);
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
catch (e) {
|
|
352
|
-
return e.code === 'EPERM';
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
367
|
/** Is a focus's OUTGOING occupant still GENERATING (a live pi actually MID-TURN)?
|
|
356
368
|
* A still-generating node is moved to backstage by a retarget (F2 — it keeps
|
|
357
369
|
* running off-screen); a holder / done / dormant / merely-parked node has its
|
|
@@ -363,7 +375,7 @@ function pidAlive(pid) {
|
|
|
363
375
|
* pid between turns; pid-alive would misclassify it as "doing work" and leave
|
|
364
376
|
* it stuck backstaged-active forever. `isBusy` is true only inside a turn, so a
|
|
365
377
|
* parked viewer reads as not-generating and is released to dormant on
|
|
366
|
-
* focus-away. The AND with `
|
|
378
|
+
* focus-away. The AND with `isPidAlive` makes a stale marker (a pi that crashed
|
|
367
379
|
* mid-turn) harmless. */
|
|
368
380
|
function isGenerating(nodeId) {
|
|
369
381
|
const row = getRow(nodeId);
|
|
@@ -371,7 +383,7 @@ function isGenerating(nodeId) {
|
|
|
371
383
|
return false;
|
|
372
384
|
if (row.status !== 'active' && row.status !== 'idle')
|
|
373
385
|
return false;
|
|
374
|
-
return isBusy(nodeId) &&
|
|
386
|
+
return isBusy(nodeId) && isPidAlive(row.pi_pid);
|
|
375
387
|
}
|
|
376
388
|
export function outgoingDisposition(o) {
|
|
377
389
|
if (!o.exists)
|
|
@@ -571,6 +583,18 @@ export function focus(nodeId, opts) {
|
|
|
571
583
|
const meta = getNode(nodeId);
|
|
572
584
|
if (meta === null)
|
|
573
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);
|
|
574
598
|
const callerPane = opts.pane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
575
599
|
if (callerPane === undefined || callerPane === '') {
|
|
576
600
|
// Not in tmux — no viewport to swap into. Reconcile and report status.
|
|
@@ -600,6 +624,105 @@ export function focus(nodeId, opts) {
|
|
|
600
624
|
}
|
|
601
625
|
return retargetFocus(f.focus_id, nodeId, opts.revive);
|
|
602
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
|
+
}
|
|
603
726
|
/** Register the caller's CURRENT pane as a focus so a `node focus`/`cycle` from a
|
|
604
727
|
* pane that isn't yet a viewport retargets IN PLACE. Occupied by whatever node
|
|
605
728
|
* sits in the pane now (`callerNode`, else resolved by pane→row), or a HOLDER
|
|
@@ -661,20 +784,28 @@ export function recycleFocusPane(nodeId, pane, launch) {
|
|
|
661
784
|
/** §1.6 lifecycle successor — hand a truly-done focused node's viewport to its
|
|
662
785
|
* manager. Repoints the focus row `focusId` to `managerId` (a DB swap of the
|
|
663
786
|
* occupant). Two takeover realizations, split on the manager's liveness:
|
|
664
|
-
* - DORMANT manager (dead pi)
|
|
665
|
-
* woken by the finished node's `push
|
|
666
|
-
* by the external daemon INTO this
|
|
667
|
-
* (remain-on-exit), where reviveIntoPlacement's
|
|
668
|
-
* 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).
|
|
669
795
|
* - LIVE manager (pi alive in the backstage, the normal multi-child state):
|
|
670
796
|
* the daemon never revives it (it only respawns dead-pi nodes), so we must
|
|
671
797
|
* bring it into the viewport SYNCHRONOUSLY here — swap its backstage pane
|
|
672
798
|
* into the focus slot (MAJOR 1). Otherwise the manager runs off-screen
|
|
673
799
|
* forever while %m sits orphaned in the viewport and the focus row lies
|
|
674
800
|
* about LOCATION.
|
|
675
|
-
* Returns false — the caller closes the focus (Q1
|
|
676
|
-
* the
|
|
677
|
-
*
|
|
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).
|
|
678
809
|
*
|
|
679
810
|
* Why the live swap is NOT the forbidden self-saw: `swap-pane -d` only EXCHANGES
|
|
680
811
|
* two panes' slot positions; it never respawns or kills the finishing node's own
|
|
@@ -690,19 +821,42 @@ export function handFocusToManager(focusId, managerId) {
|
|
|
690
821
|
return false;
|
|
691
822
|
if (getFocusByNode(managerId) !== null)
|
|
692
823
|
return false; // manager already focused elsewhere
|
|
693
|
-
setFocusOccupant(focusId, managerId);
|
|
694
|
-
// MAJOR 1 — LIVE backstage manager → swap it into the focus slot now. DORMANT
|
|
695
|
-
// managers (no live pane / dead pi) fall through unchanged: the daemon revives
|
|
696
|
-
// them into the frozen %m async.
|
|
697
824
|
const mgr = getRow(managerId);
|
|
698
|
-
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);
|
|
699
833
|
const focusLoc = paneLocation(f.pane); // F2's window/session — the slot mgr swaps INTO (%m is currently there)
|
|
700
834
|
if (swapPaneInPlace(mgr.pane, f.pane) && focusLoc !== null) {
|
|
701
835
|
setFocusPane(f.focus_id, mgr.pane, focusLoc.session); // re-anchor the focus row to mgr's pane (now in F2)
|
|
702
836
|
setPresence(managerId, { pane: mgr.pane, tmux_session: focusLoc.session, window: focusLoc.window });
|
|
703
837
|
}
|
|
838
|
+
return true; // took focus (live swap) — caller doesn't disarm
|
|
704
839
|
}
|
|
705
|
-
|
|
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;
|
|
706
860
|
}
|
|
707
861
|
/** Q1 close-to-shell for a truly-done focused node with no successor (§1.6 /
|
|
708
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
|
|
@@ -47,6 +46,9 @@ export function promote(nodeId, opts = {}) {
|
|
|
47
46
|
const { launch } = buildLaunchSpec(targetKind, 'orchestrator', {
|
|
48
47
|
lifecycle: opts.resident === true ? 'resident' : node.lifecycle,
|
|
49
48
|
hasManager: node.parent !== null,
|
|
49
|
+
// Preserve a caller-pinned model tier across the polymorph (the persona
|
|
50
|
+
// default is recomputed fresh for targetKind).
|
|
51
|
+
model: node.model_override ?? undefined,
|
|
50
52
|
});
|
|
51
53
|
// Seed a barebones roadmap scaffold if absent so the file exists for a
|
|
52
54
|
// refresh. Pre-fill its Goal from the node's goal doc when present (set at
|
|
@@ -58,13 +60,6 @@ export function promote(nodeId, opts = {}) {
|
|
|
58
60
|
seedRoadmap(nodeId, goal !== null && goal.trim() !== '' ? { goal: goal.trim() } : {});
|
|
59
61
|
roadmapWritten = true;
|
|
60
62
|
}
|
|
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
63
|
// Flip mode→orchestrator + kind + launch spec. Lifecycle is independent:
|
|
69
64
|
// only set resident when the caller asked for it (the common self-promotion
|
|
70
65
|
// stays terminal/orchestrator — it still reports up + reaps).
|
|
@@ -79,9 +74,6 @@ export function promote(nodeId, opts = {}) {
|
|
|
79
74
|
roadmapWritten,
|
|
80
75
|
roadmapPath: roadmapPath(nodeId),
|
|
81
76
|
goalPath: goalPath(nodeId),
|
|
82
|
-
memoryPath: memoryPath(nodeId),
|
|
83
|
-
userMemoryPath: userMemoryPath(),
|
|
84
|
-
projectMemoryPath: projectMemoryPath(node.cwd),
|
|
85
77
|
};
|
|
86
78
|
}
|
|
87
79
|
/** 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>;
|