@crouton-kit/crouter 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +45 -67
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -99
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +129 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +196 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +266 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +178 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +334 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +105 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +205 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +81 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +24 -12
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +26 -12
- package/dist/core/runtime/launch.js +78 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +39 -1
- package/dist/core/runtime/nodes.js +69 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +299 -0
- package/dist/core/runtime/placement.js +688 -0
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +113 -20
- package/dist/core/runtime/tmux.js +221 -39
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +594 -262
- package/dist/pi-extensions/canvas-resume.d.ts +22 -0
- package/dist/pi-extensions/canvas-resume.js +173 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +340 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +2 -2
- package/dist/core/runtime/presence.d.ts +0 -38
- package/dist/core/runtime/presence.js +0 -154
|
@@ -2,16 +2,55 @@
|
|
|
2
2
|
// window. Used by both the supervisor daemon (on crash/refresh detection) and
|
|
3
3
|
// the explicit `crtr canvas revive` command.
|
|
4
4
|
//
|
|
5
|
-
// A revive
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// A revive replays the node's persisted LaunchSpec + cwd (the canonical recipe)
|
|
6
|
+
// and routes PLACEMENT through reviveIntoPlacement (§1.4): a non-focused node
|
|
7
|
+
// opens a fresh background window in its home_session (the backstage `crtr` for
|
|
8
|
+
// a child — NEVER a user session); a node that occupies a LIVE focus resumes IN
|
|
9
|
+
// PLACE in that focus pane (respawn-pane -k, no new window). reviveNode never
|
|
10
|
+
// targets meta.tmux_session, so a background revive can no longer open an
|
|
11
|
+
// unbidden window in the user's session.
|
|
8
12
|
//
|
|
9
|
-
// resume=true → `pi --
|
|
13
|
+
// resume=true → `pi --session <path|id>` — wakes the saved conversation,
|
|
14
|
+
// preferring the absolute session-file path (cwd-immune) over
|
|
15
|
+
// the bare session id.
|
|
10
16
|
// resume=false → fresh pi invocation — the node re-reads its roadmap/context dir.
|
|
11
|
-
import { getNode, updateNode, } from '../canvas/index.js';
|
|
17
|
+
import { getNode, updateNode, setPresence, clearPid, fullName, } from '../canvas/index.js';
|
|
18
|
+
import { transition } from './lifecycle.js';
|
|
12
19
|
import { buildPiArgv } from './launch.js';
|
|
13
|
-
import { buildReviveKickoff } from './kickoff.js';
|
|
14
|
-
import {
|
|
20
|
+
import { buildReviveKickoff, drainBearings } from './kickoff.js';
|
|
21
|
+
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
22
|
+
import { reviveIntoPlacement, reconcile, isNodePaneAlive, homeSessionOf, piCommand, respawnPane, } from './placement.js';
|
|
23
|
+
import { nodeSession } from './nodes.js';
|
|
24
|
+
/** signal-0 liveness probe for a pi pid (mirrors the daemon's isPidAlive). A
|
|
25
|
+
* null pid (legacy / never-booted) reads dead. */
|
|
26
|
+
function pidAlive(pid) {
|
|
27
|
+
if (pid == null)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return e.code === 'EPERM';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// resumeArgs — which session source a revive resumes from
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/** Pick the `--session` source for a revive. resume=true prefers the absolute
|
|
41
|
+
* session-file path (immune to cwd; pi opens it directly) and keeps the bare
|
|
42
|
+
* session id as the fallback for older nodes booted before pi_session_file was
|
|
43
|
+
* captured. buildPiArgv prefers the path when both are present. resume=false (a
|
|
44
|
+
* refresh-yield) selects neither — the node re-reads its roadmap fresh. Pure so
|
|
45
|
+
* the path-vs-id selection is unit-testable without tmux. */
|
|
46
|
+
export function resumeArgs(meta, resume) {
|
|
47
|
+
if (!resume)
|
|
48
|
+
return {};
|
|
49
|
+
return {
|
|
50
|
+
resumeSessionId: meta.pi_session_id ?? undefined,
|
|
51
|
+
resumeSessionPath: meta.pi_session_file ?? undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
15
54
|
// ---------------------------------------------------------------------------
|
|
16
55
|
// reviveNode
|
|
17
56
|
// ---------------------------------------------------------------------------
|
|
@@ -25,35 +64,63 @@ export function reviveNode(nodeId, opts) {
|
|
|
25
64
|
if (meta === null) {
|
|
26
65
|
throw new Error(`reviveNode: unknown node ${nodeId}`);
|
|
27
66
|
}
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
// Double-revive guard (pane-keyed, §2.4): reconcile FIRST so a user-moved pane
|
|
68
|
+
// isn't misread as "not yet revived", then probe pane-existence. A node whose
|
|
69
|
+
// pane is alive AND whose pi is still RUNNING was already revived by another
|
|
70
|
+
// path; re-launching would put a SECOND pi on the same session file — no-op.
|
|
71
|
+
// A FROZEN focus pane (remain-on-exit, F3) is pane-alive but pi-DEAD: that is
|
|
72
|
+
// the resume-into-focus case and MUST proceed (respawn-pane -k back into the
|
|
73
|
+
// frozen pane), so the guard gates on pi liveness too, not pane-existence alone.
|
|
74
|
+
reconcile(nodeId);
|
|
75
|
+
const live = getNode(nodeId) ?? meta;
|
|
76
|
+
if (isNodePaneAlive(nodeId) && pidAlive(live.pi_pid)) {
|
|
77
|
+
return {
|
|
78
|
+
window: live.window ?? null,
|
|
79
|
+
session: live.tmux_session ?? nodeSession(),
|
|
80
|
+
resumed: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Every (re)launch is a new cycle — bump the counter so the editor label's
|
|
84
|
+
// trailing N advances. Mutate the in-memory meta too so buildPiArgv below
|
|
85
|
+
// builds the label with the incremented count.
|
|
86
|
+
meta.cycles = (meta.cycles ?? 0) + 1;
|
|
87
|
+
updateNode(nodeId, { cycles: meta.cycles });
|
|
88
|
+
// Decide whether to wake the saved pi conversation or start fresh. Prefer the
|
|
89
|
+
// absolute session-file path (cwd-immune); fall back to the bare id.
|
|
90
|
+
const resume = resumeArgs(meta, opts.resume);
|
|
91
|
+
const resuming = resume.resumeSessionPath !== undefined || resume.resumeSessionId !== undefined;
|
|
37
92
|
// A fresh revive (no resume) gets a kickoff prompt so it re-reads its roadmap
|
|
38
|
-
// and continues; resuming a saved conversation needs none.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
93
|
+
// and continues; resuming a saved conversation needs none. drainBearings is the
|
|
94
|
+
// one-shot consuming step (yield note + feed cursor + persona ack); the builder
|
|
95
|
+
// is then pure.
|
|
96
|
+
let inv;
|
|
97
|
+
if (resuming) {
|
|
98
|
+
inv = buildPiArgv(meta, resume);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const bearings = drainBearings(meta);
|
|
102
|
+
inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
|
|
103
|
+
}
|
|
104
|
+
// Placement owns WHERE this revive lands (§1.4): resume into a live focus pane
|
|
105
|
+
// if the node occupies one, else a fresh window in its home_session (the
|
|
106
|
+
// backstage `crtr` for a child — NEVER a user session). reviveIntoPlacement
|
|
107
|
+
// performs the one atomic setPresence; reviveNode keeps transition+clearPid
|
|
108
|
+
// around it (the crash-safety ordering, unchanged). THIS is the bug-kill: a
|
|
109
|
+
// non-focused background revive can no longer new-window into a user session.
|
|
110
|
+
transition(nodeId, 'revive');
|
|
111
|
+
const placed = reviveIntoPlacement(nodeId, {
|
|
48
112
|
command: piCommand(inv.argv),
|
|
113
|
+
env: inv.env,
|
|
114
|
+
cwd: meta.cwd,
|
|
115
|
+
name: fullName(meta),
|
|
116
|
+
resuming,
|
|
49
117
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
return { window, session, resumed: resumeId !== undefined };
|
|
118
|
+
// Window-backed launch: clear the stale pid so the daemon won't re-fire on
|
|
119
|
+
// it during the new pi's boot. The fresh pi re-records its pid on
|
|
120
|
+
// session_start; if it never boots, this window closes and the window-gone
|
|
121
|
+
// pass reaps it.
|
|
122
|
+
clearPid(nodeId);
|
|
123
|
+
return { window: placed.window, session: placed.session, resumed: resuming };
|
|
57
124
|
}
|
|
58
125
|
// ---------------------------------------------------------------------------
|
|
59
126
|
// reviveInPlace — refresh-yield without churning the window
|
|
@@ -67,21 +134,81 @@ export function reviveNode(nodeId, opts) {
|
|
|
67
134
|
* `pane` is the target pane id (the yielding node reads it from $TMUX_PANE).
|
|
68
135
|
* Throws on unknown node or when the respawn could not be dispatched, so the
|
|
69
136
|
* caller can fall back to a plain shutdown (daemon revives in a new window). */
|
|
70
|
-
export function reviveInPlace(nodeId, pane) {
|
|
137
|
+
export function reviveInPlace(nodeId, pane, respawn = respawnPane) {
|
|
71
138
|
const meta = getNode(nodeId);
|
|
72
139
|
if (meta === null) {
|
|
73
140
|
throw new Error(`reviveInPlace: unknown node ${nodeId}`);
|
|
74
141
|
}
|
|
142
|
+
// A refresh-yield is a cycle too — advance the label's trailing N.
|
|
143
|
+
meta.cycles = (meta.cycles ?? 0) + 1;
|
|
144
|
+
updateNode(nodeId, { cycles: meta.cycles });
|
|
145
|
+
// The node's LOCATION — the session its pane physically lives in. The re-exec
|
|
146
|
+
// is IN PLACE (the pane never moves), so this is preserved unchanged below.
|
|
75
147
|
const session = meta.tmux_session ?? nodeSession();
|
|
76
148
|
// Fresh re-exec: same recipe as a no-resume reviveNode, with the kickoff so
|
|
77
|
-
// the node rebuilds its bearings from disk.
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
149
|
+
// the node rebuilds its bearings from disk. Drain the one-shot bearings first,
|
|
150
|
+
// then build purely.
|
|
151
|
+
const bearings = drainBearings(meta);
|
|
152
|
+
const inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
|
|
153
|
+
// CRTR_ROOT_SESSION is the backstage this node's CHILDREN spawn into — it must
|
|
154
|
+
// be the durable REVIVE-HOME (home_session), NOT the pane's live `session`. A
|
|
155
|
+
// FOCUSED child's pane is in a USER session (focus taints meta.tmux_session),
|
|
156
|
+
// so sourcing it from `session` would land any child it spawns in the user's
|
|
157
|
+
// session, re-tainting that child's home_session (A-MAJOR-1). home_session is
|
|
158
|
+
// the taint-immune backstage `crtr` for a child; for a root it equals its own
|
|
159
|
+
// session, so this is behavior-preserving there.
|
|
160
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: homeSessionOf(nodeId) };
|
|
161
|
+
const ok = respawn({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
|
|
81
162
|
if (!ok) {
|
|
82
163
|
throw new Error(`reviveInPlace: respawn-pane dispatch failed for ${nodeId}`);
|
|
83
164
|
}
|
|
84
|
-
|
|
165
|
+
// Deliberately DO NOT clear intent here, and DO NOT touch pi_pid. The detached
|
|
166
|
+
// respawn-pane can't confirm it actually replaced the pi (it kills this very
|
|
167
|
+
// process mid-flight), so clearing intent optimistically is how a failed
|
|
168
|
+
// refresh became a silent death: the fresh pi never boots, yet meta says the
|
|
169
|
+
// refresh completed. Instead we leave intent='refresh' (the fresh pi clears it
|
|
170
|
+
// on boot — the only proof the respawn worked) and leave pi_pid as the OLD
|
|
171
|
+
// pid. If the respawn succeeds, the old pi dies and the fresh one overwrites
|
|
172
|
+
// pid+intent within the daemon's grace window; if it fails, the old pid stays
|
|
173
|
+
// dead and the daemon's pi-liveness pass revives the node.
|
|
174
|
+
transition(nodeId, 'boot');
|
|
175
|
+
// tmux_session may have resolved to the shared session; window is unchanged
|
|
176
|
+
// (we re-execed in place), so preserve it explicitly.
|
|
177
|
+
setPresence(nodeId, { tmux_session: session, window: meta.window ?? null });
|
|
85
178
|
// Window is unchanged (we re-execed in place); report the existing one.
|
|
86
179
|
return { window: meta.window ?? null, session, resumed: false };
|
|
87
180
|
}
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// relaunchRootInPane — boot a CLEAN fresh root in the current pane (option C)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
/** Re-exec a FRESH pi for `nodeId` in EXISTING `pane` (respawn-pane -k), with
|
|
185
|
+
* NO prompt and NO resume — a clean root conversation (goal-capture /
|
|
186
|
+
* context-intro handle the first message + bearings, exactly like bare
|
|
187
|
+
* `crtr`). Unlike reviveInPlace: no buildReviveKickoff prompt, no cycles bump,
|
|
188
|
+
* and it sets CRTR_FRONT_DOOR=1 (REQUIRED — src/core/runtime/CLAUDE.md: any
|
|
189
|
+
* path that boots a pi must guard against a removed/renamed subcommand
|
|
190
|
+
* fork-bombing). Throws if the respawn could not be dispatched.
|
|
191
|
+
*
|
|
192
|
+
* Used by relaunchRoot (reset.ts) for the `/new`-in-a-root relaunch. Kept
|
|
193
|
+
* SEPARATE from reviveInPlace so the refresh-yield path's exact semantics
|
|
194
|
+
* (kickoff + cycle bump) are untouched. */
|
|
195
|
+
export function relaunchRootInPane(nodeId, pane) {
|
|
196
|
+
const meta = getNode(nodeId);
|
|
197
|
+
if (meta === null) {
|
|
198
|
+
throw new Error(`relaunchRootInPane: unknown node ${nodeId}`);
|
|
199
|
+
}
|
|
200
|
+
// No prompt, no resume → a brand-new root conversation at cycle 0.
|
|
201
|
+
const inv = buildPiArgv(meta, {});
|
|
202
|
+
// Source CRTR_ROOT_SESSION from the durable REVIVE-HOME (home_session), the
|
|
203
|
+
// same taint-immunity rule as reviveInPlace. relaunchRootInPane runs only on a
|
|
204
|
+
// root, whose home_session IS its own session, so this is behavior-preserving
|
|
205
|
+
// — it keeps both in-pane revive paths sourced identically.
|
|
206
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: homeSessionOf(nodeId), [FRONT_DOOR_ENV]: '1' };
|
|
207
|
+
const ok = respawnPane({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
|
|
208
|
+
if (!ok) {
|
|
209
|
+
throw new Error(`relaunchRootInPane: respawn-pane dispatch failed for ${nodeId}`);
|
|
210
|
+
}
|
|
211
|
+
// Do NOT clear intent/pi_pid here — the fresh pi clears intent='refresh' on
|
|
212
|
+
// its session_start boot (the only proof the respawn worked), same dance as
|
|
213
|
+
// reviveInPlace.
|
|
214
|
+
}
|
|
@@ -5,9 +5,6 @@ export interface BootRootOpts {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
/** Optional starter prompt (bare `crtr` requires none). */
|
|
7
7
|
prompt?: string;
|
|
8
|
-
/** 'inline' — exec pi in the current terminal (bare `crtr`).
|
|
9
|
-
* 'session' — create a dedicated tmux session and run pi there (`session new`). */
|
|
10
|
-
placement: 'inline' | 'session';
|
|
11
8
|
}
|
|
12
9
|
/** Create a root node and bring up its pi. Returns the node; for 'inline' this
|
|
13
10
|
* only returns after pi exits (it took over the terminal). */
|
|
@@ -20,12 +17,30 @@ export interface SpawnChildOpts {
|
|
|
20
17
|
prompt: string;
|
|
21
18
|
/** Override the parent (defaults to the calling node from env). */
|
|
22
19
|
parent?: string;
|
|
20
|
+
/** Spawn an INDEPENDENT root instead of a managed child: parent=null, no
|
|
21
|
+
* subscription back to the spawner, resident lifecycle, spawned_by=spawner.
|
|
22
|
+
* Brought forefront on spawn so a human can drive it directly. */
|
|
23
|
+
root?: boolean;
|
|
24
|
+
/** Fork the new node from an existing pi conversation instead of starting it
|
|
25
|
+
* fresh: a node id (resolved to that node's session file), an absolute
|
|
26
|
+
* `.jsonl` path, or a partial pi session uuid. pi COPIES that history into a
|
|
27
|
+
* new session for the child — the source is untouched — then `prompt` is the
|
|
28
|
+
* next message. A one-shot at birth; the child resumes its own session after. */
|
|
29
|
+
forkFrom?: string;
|
|
23
30
|
}
|
|
31
|
+
/** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
|
|
32
|
+
* A live node id resolves to its captured session FILE (absolute, cwd-immune),
|
|
33
|
+
* falling back to its bare session id; a path or partial uuid passes straight
|
|
34
|
+
* through to pi. Throws when a known node has no session to fork yet. */
|
|
35
|
+
export declare function resolveForkSource(value: string): string;
|
|
24
36
|
export interface SpawnChildResult {
|
|
25
37
|
node: NodeMeta;
|
|
26
38
|
window: string | null;
|
|
27
39
|
session: string;
|
|
28
40
|
}
|
|
29
|
-
/** Spawn a
|
|
30
|
-
*
|
|
41
|
+
/** Spawn a node from a live node. By default a managed terminal worker in a
|
|
42
|
+
* background window, with the spawner auto-subscribed (active) via spawnNode.
|
|
43
|
+
* With `root`: an independent resident root — parent=null, NO subscription back
|
|
44
|
+
* to the spawner (it carries spawned_by=spawner for provenance only), brought
|
|
45
|
+
* forefront so a human can pick up the conversation directly. */
|
|
31
46
|
export declare function spawnChild(opts: SpawnChildOpts): SpawnChildResult;
|
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
// a running pi process on the canvas. Composes canvas (birth + spine), persona
|
|
3
3
|
// (resolve), launch (pi argv), and tmux (placement).
|
|
4
4
|
//
|
|
5
|
-
// bootRoot —
|
|
6
|
-
//
|
|
7
|
-
// spawnChild — a
|
|
8
|
-
//
|
|
5
|
+
// bootRoot — the user-opened front door (bare `crtr`). Resident; runs pi
|
|
6
|
+
// inline, taking over the current terminal.
|
|
7
|
+
// spawnChild — a node spawned by a live node (`crtr node new`). A managed,
|
|
8
|
+
// terminal background worker by default; with `root`, an
|
|
9
|
+
// INDEPENDENT resident root (no subscription back to the spawner,
|
|
10
|
+
// provenance via spawned_by) brought forefront for direct driving.
|
|
9
11
|
import { spawnSync } from 'node:child_process';
|
|
10
12
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
11
|
-
import { spawnNode, currentNodeContext } from './nodes.js';
|
|
13
|
+
import { spawnNode, currentNodeContext, resolveBirthSession, nodeSession } from './nodes.js';
|
|
12
14
|
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
13
15
|
import { writeGoal } from './kickoff.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
+
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
17
|
+
import { seedMemory, seedUserMemory, seedProjectMemory } from './memory.js';
|
|
18
|
+
import { generateSessionName } from './naming.js';
|
|
19
|
+
import { installMenuBinding, installNavBindings } from './tmux-chrome.js';
|
|
20
|
+
import { setPresence, updateNode, getNode, fullName } from '../canvas/index.js';
|
|
21
|
+
import { registerRootFocus, ensureSession, openNodeWindow, piCommand, currentTmux, inTmux, focusWindow, } from './placement.js';
|
|
22
|
+
import { transition } from './lifecycle.js';
|
|
16
23
|
import { ensureDaemon } from '../../daemon/manage.js';
|
|
17
24
|
/** Create a root node and bring up its pi. Returns the node; for 'inline' this
|
|
18
25
|
* only returns after pi exits (it took over the terminal). */
|
|
@@ -26,13 +33,20 @@ export function bootRoot(opts) {
|
|
|
26
33
|
const kind = opts.kind ?? 'general';
|
|
27
34
|
// A born-resident root starts in base mode; it earns the orchestrator persona
|
|
28
35
|
// the first time it delegates (or on promotion). Resident lifecycle either way.
|
|
29
|
-
const { launch } = buildLaunchSpec(kind, 'base');
|
|
36
|
+
const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
37
|
+
// A root opened WITH a prompt gets its editor name now (so the first pi
|
|
38
|
+
// session already carries it). A bare root has no prompt yet — the
|
|
39
|
+
// goal-capture extension names it from the first message (async, next cycle).
|
|
40
|
+
const description = opts.prompt !== undefined && opts.prompt.trim() !== ''
|
|
41
|
+
? generateSessionName(opts.prompt)
|
|
42
|
+
: undefined;
|
|
30
43
|
const meta = spawnNode({
|
|
31
44
|
kind,
|
|
32
45
|
mode: 'base',
|
|
33
46
|
lifecycle: 'resident',
|
|
34
47
|
cwd: opts.cwd,
|
|
35
48
|
name: opts.name ?? kind,
|
|
49
|
+
description,
|
|
36
50
|
parent: null,
|
|
37
51
|
launch,
|
|
38
52
|
});
|
|
@@ -55,74 +69,180 @@ export function bootRoot(opts) {
|
|
|
55
69
|
}
|
|
56
70
|
catch { /* best-effort */ }
|
|
57
71
|
}
|
|
58
|
-
if (opts.placement === 'session') {
|
|
59
|
-
updateNode(meta.node_id, { tmux_session: session });
|
|
60
|
-
const withSession = getNode(meta.node_id);
|
|
61
|
-
const inv = buildPiArgv(withSession, { prompt: opts.prompt });
|
|
62
|
-
const env = { ...inv.env, CRTR_ROOT_SESSION: session, [FRONT_DOOR_ENV]: '1' };
|
|
63
|
-
const win = openNodeWindow({
|
|
64
|
-
session,
|
|
65
|
-
name: meta.name,
|
|
66
|
-
cwd: opts.cwd,
|
|
67
|
-
env,
|
|
68
|
-
command: piCommand(inv.argv),
|
|
69
|
-
});
|
|
70
|
-
updateNode(meta.node_id, { window: win });
|
|
71
|
-
return getNode(meta.node_id);
|
|
72
|
-
}
|
|
73
72
|
// inline: the root's pi takes over THIS terminal, so its own window stays
|
|
74
73
|
// where the user is (its tmux_session tracks that real pane so supervision
|
|
75
74
|
// sees it alive). But its children spawn into the shared global session via
|
|
76
75
|
// CRTR_ROOT_SESSION — they never clutter the user's working session.
|
|
77
76
|
const here = currentTmux();
|
|
78
|
-
const adopted = here
|
|
79
|
-
|
|
77
|
+
const adopted = resolveBirthSession({ adoptCaller: true, here, rootSession: undefined });
|
|
78
|
+
setPresence(meta.node_id, { tmux_session: adopted, window: here?.window ?? null, pane: here?.pane ?? null });
|
|
79
|
+
// REVIVE-HOME: the inline root's durable revive target is the session it
|
|
80
|
+
// adopts (the caller's when inside tmux, else the shared backstage). Set once
|
|
81
|
+
// at birth, alongside the live LOCATION above.
|
|
82
|
+
updateNode(meta.node_id, { home_session: adopted });
|
|
83
|
+
// Root boot registers focus #1 (§2.6): the FOREGROUND inline root owns the
|
|
84
|
+
// user's viewport, so its OWN pane becomes a durable focus (remain-on-exit so
|
|
85
|
+
// a clean exit freezes rather than detaching the terminal). A background
|
|
86
|
+
// `--root` (spawnChild) does NOT — it stays a plain window until the user
|
|
87
|
+
// focuses it (§6). Only possible inside tmux (a pane to anchor on).
|
|
88
|
+
if (here) {
|
|
89
|
+
try {
|
|
90
|
+
registerRootFocus(meta.node_id, here.pane, adopted, here.window);
|
|
91
|
+
}
|
|
92
|
+
catch { /* best-effort */ }
|
|
93
|
+
}
|
|
80
94
|
const withSession = getNode(meta.node_id);
|
|
81
95
|
const inv = buildPiArgv(withSession, { prompt: opts.prompt });
|
|
82
96
|
const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: session, [FRONT_DOOR_ENV]: '1' };
|
|
83
97
|
const r = spawnSync('pi', inv.argv, { stdio: 'inherit', env });
|
|
84
98
|
process.exit(r.status ?? 0);
|
|
85
99
|
}
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
100
|
+
/** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
|
|
101
|
+
* A live node id resolves to its captured session FILE (absolute, cwd-immune),
|
|
102
|
+
* falling back to its bare session id; a path or partial uuid passes straight
|
|
103
|
+
* through to pi. Throws when a known node has no session to fork yet. */
|
|
104
|
+
export function resolveForkSource(value) {
|
|
105
|
+
const v = value.trim();
|
|
106
|
+
if (v === '')
|
|
107
|
+
throw new Error('--fork-from requires a node id, session file, or session uuid.');
|
|
108
|
+
// A path (contains `/` or ends `.jsonl`) is a session file — hand it to pi as-is.
|
|
109
|
+
if (v.includes('/') || v.endsWith('.jsonl'))
|
|
110
|
+
return v;
|
|
111
|
+
// A live node id — fork from the conversation it has accumulated.
|
|
112
|
+
const n = getNode(v);
|
|
113
|
+
if (n !== null) {
|
|
114
|
+
const src = n.pi_session_file ?? n.pi_session_id;
|
|
115
|
+
if (src === undefined || src === null || src === '') {
|
|
116
|
+
throw new Error(`node ${v} has no pi session yet — it has not started a conversation to fork from.`);
|
|
117
|
+
}
|
|
118
|
+
return src;
|
|
119
|
+
}
|
|
120
|
+
// Not a known node — treat as a bare/partial pi session id for pi to resolve.
|
|
121
|
+
return v;
|
|
122
|
+
}
|
|
123
|
+
/** Spawn a node from a live node. By default a managed terminal worker in a
|
|
124
|
+
* background window, with the spawner auto-subscribed (active) via spawnNode.
|
|
125
|
+
* With `root`: an independent resident root — parent=null, NO subscription back
|
|
126
|
+
* to the spawner (it carries spawned_by=spawner for provenance only), brought
|
|
127
|
+
* forefront so a human can pick up the conversation directly. */
|
|
88
128
|
export function spawnChild(opts) {
|
|
89
129
|
try {
|
|
90
130
|
ensureDaemon();
|
|
91
131
|
}
|
|
92
132
|
catch { /* daemon is best-effort */ }
|
|
93
133
|
const ctx = currentNodeContext();
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
134
|
+
const spawner = opts.parent ?? ctx.nodeId;
|
|
135
|
+
if (spawner === null || spawner === undefined) {
|
|
96
136
|
throw new Error('spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent');
|
|
97
137
|
}
|
|
138
|
+
const root = opts.root === true;
|
|
98
139
|
const mode = opts.mode ?? 'base';
|
|
99
|
-
|
|
140
|
+
// Lifecycle keys on ROOT-ness only, independent of mode: an independent root
|
|
141
|
+
// (or `--root`) is resident (a conversation that persists, woken by inbox/
|
|
142
|
+
// human); every spawned child is terminal — it owes a final up the spine and
|
|
143
|
+
// reaps when done. A child born as an orchestrator is terminal/orchestrator
|
|
144
|
+
// (delegates + holds a roadmap, but still reports up), NOT resident.
|
|
145
|
+
const lifecycle = root ? 'resident' : 'terminal';
|
|
146
|
+
// Spine: a managed child reports up to its spawner (has a manager); an
|
|
147
|
+
// independent root sits top-of-spine with nobody to push to. Mirrors the
|
|
148
|
+
// `parent` set below (root ? null : spawner), so hasManager === parent!==null.
|
|
149
|
+
const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root });
|
|
150
|
+
// Name the worker from its task now, so its first editor label carries it.
|
|
100
151
|
const meta = spawnNode({
|
|
101
152
|
kind: opts.kind,
|
|
102
153
|
mode,
|
|
103
|
-
lifecycle
|
|
154
|
+
lifecycle,
|
|
104
155
|
cwd: opts.cwd,
|
|
105
156
|
name: opts.name ?? opts.kind,
|
|
106
|
-
|
|
157
|
+
description: generateSessionName(opts.prompt),
|
|
158
|
+
// A root has no spine parent (top-level, nobody subscribes); it still
|
|
159
|
+
// records spawned_by=spawner. A child's parent IS its manager.
|
|
160
|
+
parent: root ? null : spawner,
|
|
161
|
+
spawnedBy: root ? spawner : undefined,
|
|
107
162
|
launch,
|
|
108
163
|
});
|
|
109
164
|
// Persist the task as the child's goal for a fresh revive to re-read.
|
|
110
165
|
writeGoal(meta.node_id, opts.prompt);
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
166
|
+
// A fork copies an existing conversation into this child's first session
|
|
167
|
+
// (resolved to an absolute file path when forking from a node). Resolved here
|
|
168
|
+
// — not in buildPiArgv — so a bad reference fails the spawn loudly before any
|
|
169
|
+
// window opens, rather than after pi is already booting.
|
|
170
|
+
const forkFrom = opts.forkFrom !== undefined ? resolveForkSource(opts.forkFrom) : undefined;
|
|
171
|
+
// A child created DIRECTLY as an orchestrator (mode='orchestrator') boots
|
|
172
|
+
// with the orchestrator persona but bypasses promote(), which is where a
|
|
173
|
+
// roadmap scaffold would normally be seeded. Lay one down here (goal
|
|
174
|
+
// pre-filled from the task) so the orchestrator has its memory artifact from
|
|
175
|
+
// birth, instead of waking memory-less. Guarded so it never clobbers.
|
|
176
|
+
if (mode === 'orchestrator' && !hasRoadmap(meta.node_id)) {
|
|
177
|
+
seedRoadmap(meta.node_id, { goal: opts.prompt.trim() });
|
|
178
|
+
}
|
|
179
|
+
// Born an orchestrator ⇒ also lay down its three scoped long-term memory
|
|
180
|
+
// stores, the companions to the roadmap: user-global (key-less), project
|
|
181
|
+
// (keyed off the child's cwd), and node-local. Each guarded against clobber.
|
|
182
|
+
if (mode === 'orchestrator') {
|
|
183
|
+
seedUserMemory();
|
|
184
|
+
seedProjectMemory(opts.cwd);
|
|
185
|
+
seedMemory(meta.node_id);
|
|
186
|
+
}
|
|
187
|
+
// A managed CHILD lands in the shared global session: inherited from the
|
|
188
|
+
// parent's CRTR_ROOT_SESSION, else the default node session. A --root spawned
|
|
189
|
+
// from inside tmux instead opens its window in the CALLER'S CURRENT session,
|
|
190
|
+
// so it appears where the spawner is working rather than exiled to a separate
|
|
191
|
+
// crtr session. Either way the root's OWN descendants still flow to the shared
|
|
192
|
+
// session (childSession) via CRTR_ROOT_SESSION, to keep the subtree from
|
|
193
|
+
// cluttering the user's session.
|
|
194
|
+
const rootSessionEnv = process.env['CRTR_ROOT_SESSION'];
|
|
195
|
+
const here = root ? currentTmux() : null;
|
|
196
|
+
// The shared backstage the whole subtree flows into (this child's own
|
|
197
|
+
// CRTR_ROOT_SESSION): the inherited root session, else the default `crtr`.
|
|
198
|
+
const childSession = resolveBirthSession({ adoptCaller: false, here, rootSession: rootSessionEnv });
|
|
199
|
+
// Where THIS node's window opens — and its durable REVIVE-HOME. A managed
|
|
200
|
+
// child lands in the backstage; a --root adopts the caller's current session
|
|
201
|
+
// when inside tmux, so it appears where the spawner is working.
|
|
202
|
+
const session = resolveBirthSession({ adoptCaller: root, here, rootSession: rootSessionEnv });
|
|
116
203
|
ensureSession(session, opts.cwd);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
204
|
+
// REVIVE-HOME set once at birth: a managed child's revive target is the
|
|
205
|
+
// backstage, never a user session — this is what keeps a background revive
|
|
206
|
+
// off the user's screen (the focus taint cannot reach it).
|
|
207
|
+
updateNode(meta.node_id, { home_session: session });
|
|
208
|
+
const inv = buildPiArgv(meta, { prompt: opts.prompt, forkFrom });
|
|
209
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: childSession, [FRONT_DOOR_ENV]: '1' };
|
|
210
|
+
const command = piCommand(inv.argv);
|
|
211
|
+
// openNodeWindow now returns {window, pane}; pane is unused until the
|
|
212
|
+
// placement layer lands, so destructure the window and proceed unchanged.
|
|
213
|
+
const opened = openNodeWindow({
|
|
120
214
|
session,
|
|
121
|
-
name: meta
|
|
215
|
+
name: fullName(meta),
|
|
122
216
|
cwd: opts.cwd,
|
|
123
217
|
env,
|
|
124
|
-
command
|
|
218
|
+
command,
|
|
125
219
|
});
|
|
126
|
-
const
|
|
220
|
+
const window = opened?.window ?? null;
|
|
221
|
+
// Two-stage failure model. Opening the window is instant and definitive, so a
|
|
222
|
+
// failure here is reported SYNCHRONOUSLY: crash the node (so it isn't a zombie
|
|
223
|
+
// 'active' the daemon can't reap — it has no window to watch) and throw so
|
|
224
|
+
// `crtr node new` exits non-zero with a clear message for the caller. The node
|
|
225
|
+
// is still 'active' from spawnNode, so transition('crash') is a legal from-LIVE
|
|
226
|
+
// move — the last scattered node-status write, now through the lifecycle machine.
|
|
227
|
+
//
|
|
228
|
+
// pi BOOTING inside the window, by contrast, is inherently slow (and slower
|
|
229
|
+
// under load), so we stay optimistic and return status='active' the instant
|
|
230
|
+
// the window exists. A vehicle that then dies before its first session_start
|
|
231
|
+
// is caught by the daemon — it surfaces the boot failure up the spine rather
|
|
232
|
+
// than letting the node die silently (see crtrd.ts surfaceBootFailure).
|
|
233
|
+
if (window === null) {
|
|
234
|
+
transition(meta.node_id, 'crash');
|
|
235
|
+
throw new Error(`failed to open a tmux window for ${meta.node_id} (${meta.name}) in session '${session}' — the node was not started.`);
|
|
236
|
+
}
|
|
237
|
+
setPresence(meta.node_id, { tmux_session: session, window });
|
|
238
|
+
const saved = getNode(meta.node_id);
|
|
239
|
+
// A root is spawned to be driven directly — bring it forefront so whoever
|
|
240
|
+
// asked for it picks up the conversation. A child stays a background window.
|
|
241
|
+
if (root) {
|
|
242
|
+
try {
|
|
243
|
+
focusWindow(session, window);
|
|
244
|
+
}
|
|
245
|
+
catch { /* best-effort */ }
|
|
246
|
+
}
|
|
127
247
|
return { node: saved, window, session };
|
|
128
248
|
}
|
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
// subscription to a node that's still live (active|idle) — something that can
|
|
6
6
|
// actually wake it. (A passive sub won't wake you, so it doesn't count.)
|
|
7
7
|
//
|
|
8
|
-
// •
|
|
9
|
-
//
|
|
8
|
+
// • resident → an interactable / human-driven node is NEVER forced to
|
|
9
|
+
// submit a final: stopping to go dormant is always
|
|
10
|
+
// legitimate (woken by inbox/human). Keyed on the LIFECYCLE
|
|
11
|
+
// value, not on parent/mode — what matters is residency.
|
|
12
|
+
// • waiting → a TERMINAL node holding an active live subscription is a
|
|
13
|
+
// dormant orchestrator awaiting its workers. Let it sleep;
|
|
14
|
+
// a child's push wakes it (and idle-releases its window).
|
|
10
15
|
// • finished/asked → it pushed --final (done) or called `crtr ask` this turn.
|
|
11
16
|
// Also fine.
|
|
12
|
-
// • otherwise →
|
|
13
|
-
// Re-prompt it to finish or escalate.
|
|
17
|
+
// • otherwise → a TERMINAL node with nothing live to wait for and no
|
|
18
|
+
// final pushed. Re-prompt it to finish or escalate.
|
|
14
19
|
import { hasActiveLiveSubscription, getNode } from '../canvas/index.js';
|
|
15
20
|
export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
|
|
16
21
|
'Run `crtr push final "<result>"` if the work is done, or `crtr human ask` if you are blocked or need the user.';
|
|
@@ -21,13 +26,18 @@ export function evaluateStop(nodeId, signals) {
|
|
|
21
26
|
return { action: 'allow', reason: 'finished' };
|
|
22
27
|
if (signals.askedHuman)
|
|
23
28
|
return { action: 'allow', reason: 'escalated' };
|
|
24
|
-
// A
|
|
25
|
-
//
|
|
29
|
+
// A RESIDENT node is interactable / human-driven and is never forced to submit
|
|
30
|
+
// a final: stopping to go dormant is always legitimate (the inbox or the human
|
|
31
|
+
// wakes it). Keyed on lifecycle, not parent — whether it has a parent doesn't
|
|
32
|
+
// matter, only whether it's resident. Roots are resident by birth default, so
|
|
33
|
+
// this still covers "don't nag the human's root" while generalizing it.
|
|
26
34
|
const node = getNode(nodeId);
|
|
27
|
-
if (node !== null &&
|
|
28
|
-
return { action: 'allow', reason: '
|
|
35
|
+
if (node !== null && node.lifecycle === 'resident') {
|
|
36
|
+
return { action: 'allow', reason: 'dormant' };
|
|
29
37
|
}
|
|
38
|
+
// A terminal node holding something live to wake it is legitimately awaiting.
|
|
30
39
|
if (hasActiveLiveSubscription(nodeId))
|
|
31
40
|
return { action: 'allow', reason: 'awaiting' };
|
|
41
|
+
// A terminal node with nothing live and no final pushed has stalled.
|
|
32
42
|
return { action: 'reprompt', reason: 'stalled', message: STALL_REPROMPT };
|
|
33
43
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { installMenuBinding, installNavBindings, sendKeysEnter } from './tmux.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// tmux-chrome.ts — chrome seam (§2.1): stateless keybind/input verbs.
|
|
2
|
+
// The ONLY non-placement module allowed to import the tmux driver, per the
|
|
3
|
+
// §5.1 lint. Re-exports the menu/nav/send-keys verbs callers (spawn, chord) need.
|
|
4
|
+
export { installMenuBinding, installNavBindings, sendKeysEnter } from './tmux.js';
|