@crouton-kit/crouter 0.3.14 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/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 +44 -66
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -98
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +130 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +259 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.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 +244 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +183 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +328 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +106 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/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 +27 -10
- 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 +24 -12
- package/dist/core/runtime/launch.js +75 -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 +32 -1
- package/dist/core/runtime/nodes.js +60 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -15
- package/dist/core/runtime/presence.js +90 -66
- 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.d.ts +100 -14
- package/dist/core/runtime/tmux.js +201 -28
- 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 +586 -262
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
|
@@ -1,67 +1,91 @@
|
|
|
1
|
-
// Root reset — the `/new`
|
|
1
|
+
// Root reset + relaunch — the `/new` equivalents, plus clean-exit termination.
|
|
2
2
|
//
|
|
3
3
|
// A live pi process is bound to one node via CRTR_NODE_ID (set at launch, not
|
|
4
|
-
// rebindable mid-process). When the user runs `/new`, the conversation
|
|
5
|
-
// but the process — and thus the node id — stays the same. To make `/new`
|
|
6
|
-
// behave like re-running `crtr`
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
4
|
+
// rebindable mid-process). When the user runs `/new`, the conversation resets
|
|
5
|
+
// but the OS process — and thus the node id — stays the same. To make `/new`
|
|
6
|
+
// behave like re-running `crtr` we have two strategies:
|
|
7
|
+
//
|
|
8
|
+
// • relaunchRoot (option C) — for a ROOT in a tmux pane: PARK the old root
|
|
9
|
+
// (mark done, keep its id/edges/pi_session_id intact as history), mint a
|
|
10
|
+
// FRESH node id, and re-exec pi in the current pane bound to the new id.
|
|
11
|
+
// The old id never changes meaning; external refs stay valid.
|
|
12
|
+
// • resetRoot (fallback) — for a non-root child (session-id refresh only) or
|
|
13
|
+
// a root with no pane (no tmux): the legacy in-place reset of the SAME id.
|
|
14
|
+
//
|
|
15
|
+
// Termination semantics: a pi that ends cleanly resolves its node to `done`
|
|
16
|
+
// (markCleanExitDone); only a true crash leaves it `dead`. A force-kill
|
|
17
|
+
// (closeWindow / respawn-pane -k) fires NO clean session_shutdown, so reaped
|
|
18
|
+
// descendants are marked `done` explicitly here.
|
|
11
19
|
//
|
|
12
20
|
// Best-effort throughout: a tmux/fs failure on one node never aborts the reset.
|
|
13
21
|
import { existsSync, rmSync } from 'node:fs';
|
|
14
|
-
import { getNode, updateNode,
|
|
15
|
-
import {
|
|
22
|
+
import { getNode, updateNode, setPresence, clearPid, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, nodeDir, openDb, } from '../canvas/index.js';
|
|
23
|
+
import { transition } from './lifecycle.js';
|
|
24
|
+
import { paneLocation, nodeSession } from './tmux.js';
|
|
25
|
+
import { tearDownNode } from './placement.js';
|
|
16
26
|
import { buildLaunchSpec } from './launch.js';
|
|
17
27
|
import { roadmapPath } from './roadmap.js';
|
|
18
|
-
|
|
28
|
+
import { spawnNode, newNodeId } from './nodes.js';
|
|
29
|
+
import { setFocus } from './presence.js';
|
|
30
|
+
import { relaunchRootInPane } from './revive.js';
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// reapDescendants — tear down a root's descendant sub-DAG (shared helper)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/** Reap the descendant sub-DAG of `rootId`: mark each **done** (the user moved
|
|
35
|
+
* on — a clean teardown, NOT a fault) + clear intent FIRST, then kill its
|
|
36
|
+
* window (closes the daemon revive race). Edges are LEFT INTACT — descendants
|
|
37
|
+
* keep parent=rootId. No wipe. Returns the reaped ids.
|
|
38
|
+
*
|
|
39
|
+
* Why `done`, and why marking is STILL explicit: a `closeWindow`/`respawn-pane
|
|
40
|
+
* -k` kill is abrupt and fires NO clean `session_shutdown`, so the general
|
|
41
|
+
* quit→done rule does NOT auto-resolve a force-killed descendant — we mark it
|
|
42
|
+
* `done` here. Shared by relaunchRoot (option C) and resetRoot's in-place
|
|
43
|
+
* fallback, so both leave their descendants `done`. */
|
|
44
|
+
export function reapDescendants(rootId) {
|
|
45
|
+
const reaped = [];
|
|
46
|
+
for (const id of view(rootId)) {
|
|
47
|
+
try {
|
|
48
|
+
// Reap BEFORE tearing down the placement (the crash-safety invariant the
|
|
49
|
+
// `reap` event encodes): a non-supervised status + cleared intent first, so
|
|
50
|
+
// the daemon can't revive a descendant mid-teardown. tearDownNode then
|
|
51
|
+
// closes any focus row it held, kills its pane (pane-keyed), and nulls its
|
|
52
|
+
// LOCATION.
|
|
53
|
+
transition(id, 'reap');
|
|
54
|
+
tearDownNode(id);
|
|
55
|
+
reaped.push(id);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* one bad node never aborts the reap */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return reaped;
|
|
62
|
+
}
|
|
63
|
+
/** Reset a root node to a pristine, empty graph (the legacy `/new` semantics —
|
|
64
|
+
* now used as the no-pane fallback and the non-root session-id refresh).
|
|
19
65
|
*
|
|
20
66
|
* For a non-root (spawned child), a `/new` is not a graph reset — we only
|
|
21
|
-
* refresh its session id so a later `--
|
|
22
|
-
export function resetRoot(nodeId, newSessionId) {
|
|
67
|
+
* refresh its session id so a later `--session <id>` wakes the right conversation. */
|
|
68
|
+
export function resetRoot(nodeId, newSessionId, newSessionFile) {
|
|
23
69
|
const meta = getNode(nodeId);
|
|
24
70
|
if (meta === null)
|
|
25
71
|
return { reaped: [], detached: [], reset: false };
|
|
26
72
|
// Only roots own a graph in the "ran crtr again" sense.
|
|
27
73
|
if (meta.parent != null) {
|
|
28
74
|
if (newSessionId !== undefined) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
75
|
+
updateNode(nodeId, {
|
|
76
|
+
pi_session_id: newSessionId,
|
|
77
|
+
...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
|
|
78
|
+
});
|
|
33
79
|
}
|
|
34
80
|
return { reaped: [], detached: [], reset: false };
|
|
35
81
|
}
|
|
36
|
-
// 1) Reap the descendant sub-DAG
|
|
37
|
-
|
|
38
|
-
// (or 'idle-release'), so flipping to dead before the window dies closes
|
|
39
|
-
// the race where a descendant mid-yield gets revived as we tear it down.
|
|
40
|
-
const reaped = [];
|
|
41
|
-
for (const id of view(nodeId)) {
|
|
42
|
-
try {
|
|
43
|
-
const dmeta = getNode(id);
|
|
44
|
-
setStatus(id, 'dead');
|
|
45
|
-
updateNode(id, { intent: null });
|
|
46
|
-
if (dmeta !== null && windowAlive(dmeta.tmux_session, dmeta.window)) {
|
|
47
|
-
closeWindow(dmeta.window);
|
|
48
|
-
}
|
|
49
|
-
reaped.push(id);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
/* one bad node never aborts the reset */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
82
|
+
// 1) Reap the descendant sub-DAG (mark done + kill windows; shared helper).
|
|
83
|
+
const reaped = reapDescendants(nodeId);
|
|
55
84
|
// 2) Detach the root's own subscriptions so its view is empty.
|
|
56
85
|
const detached = [];
|
|
57
86
|
for (const sub of subscriptionsOf(nodeId)) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
detached.push(sub.node_id);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
/* */
|
|
64
|
-
}
|
|
87
|
+
unsubscribe(nodeId, sub.node_id);
|
|
88
|
+
detached.push(sub.node_id);
|
|
65
89
|
}
|
|
66
90
|
// 3) Wipe the root's working state (reports / inbox / roadmap).
|
|
67
91
|
for (const p of [
|
|
@@ -78,20 +102,167 @@ export function resetRoot(nodeId, newSessionId) {
|
|
|
78
102
|
/* */
|
|
79
103
|
}
|
|
80
104
|
}
|
|
81
|
-
// 4) Re-point the root at a fresh base persona + the new pi session id.
|
|
105
|
+
// 4) Re-point the root at a fresh base persona + the new pi session id. A
|
|
106
|
+
// root is resident by definition (this only runs on roots — see the early
|
|
107
|
+
// return above), so resetting to base/resident is the model, not a bypass.
|
|
108
|
+
// Re-seed persona_ack to the fresh persona so the pristine `/new`
|
|
109
|
+
// conversation never gets a spurious mode/lifecycle transition steer (the
|
|
110
|
+
// persona injector compares against this ack).
|
|
111
|
+
const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
112
|
+
updateNode(nodeId, {
|
|
113
|
+
mode: 'base',
|
|
114
|
+
lifecycle: 'resident',
|
|
115
|
+
persona_ack: { mode: 'base', lifecycle: 'resident' },
|
|
116
|
+
launch,
|
|
117
|
+
...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
|
|
118
|
+
...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
|
|
119
|
+
});
|
|
120
|
+
transition(nodeId, 'revive');
|
|
121
|
+
return { reaped, detached, reset: true };
|
|
122
|
+
}
|
|
123
|
+
/** The single entry the stophook calls on a detected `/new` (session id change).
|
|
124
|
+
* Policy lives here so the stophook stays thin and this stays unit-testable:
|
|
125
|
+
* - non-root child → resetRoot(nodeId, newSessionId) (session-id refresh only)
|
|
126
|
+
* - root + pane present → relaunchRoot(nodeId, pane) (option C)
|
|
127
|
+
* - root + no pane (no tmux) → resetRoot(nodeId, newSessionId) (in-place fallback)
|
|
128
|
+
* On a respawn-dispatch failure the live pi never died, so we degrade to the
|
|
129
|
+
* legacy in-place reset. */
|
|
130
|
+
export function handleNewSession(nodeId, newSessionId, pane, deps = {}, newSessionFile) {
|
|
131
|
+
const meta = getNode(nodeId);
|
|
132
|
+
if (meta === null)
|
|
133
|
+
return { path: 'noop' };
|
|
134
|
+
// Non-root child: a `/new` only refreshes its session id (unchanged).
|
|
135
|
+
if (meta.parent != null) {
|
|
136
|
+
resetRoot(nodeId, newSessionId, newSessionFile);
|
|
137
|
+
return { path: 'reset-child' };
|
|
138
|
+
}
|
|
139
|
+
// Root with no pane (not inside tmux): in-place reset fallback. Option C needs
|
|
140
|
+
// a pane to respawn into; resetRoot needs the new session id (available here
|
|
141
|
+
// because the trigger is session_start).
|
|
142
|
+
if (pane === undefined || pane.trim() === '') {
|
|
143
|
+
resetRoot(nodeId, newSessionId, newSessionFile);
|
|
144
|
+
return { path: 'reset-root' };
|
|
145
|
+
}
|
|
146
|
+
// Root with a pane: option C relaunch. relaunchRoot self-rolls-back its DB
|
|
147
|
+
// writes on a respawn-dispatch failure and rethrows; we then degrade to the
|
|
148
|
+
// legacy in-place reset (the live pi is still alive, never killed).
|
|
149
|
+
try {
|
|
150
|
+
const result = relaunchRoot(nodeId, pane, deps);
|
|
151
|
+
if (result === null)
|
|
152
|
+
return { path: 'noop' }; // defensive guard hit (e.g. rapid double /new)
|
|
153
|
+
return { path: 'relaunch', newNodeId: result.newNodeId };
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
setFocus(nodeId);
|
|
157
|
+
resetRoot(nodeId, newSessionId, newSessionFile);
|
|
158
|
+
return { path: 'reset-root' };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Park the old root + create+launch a fresh root in `pane` (option C). All DB
|
|
162
|
+
* writes are synchronous and happen BEFORE the respawn (the respawn kills the
|
|
163
|
+
* caller). Returns the new node id, or null on a defensive guard (not a root /
|
|
164
|
+
* already parked). Throws only if the respawn dispatch fails — and self-rolls-
|
|
165
|
+
* back its writes first so the caller can degrade to resetRoot. */
|
|
166
|
+
export function relaunchRoot(oldId, pane, deps = {}) {
|
|
167
|
+
const oldMeta = getNode(oldId);
|
|
168
|
+
if (oldMeta === null || oldMeta.parent != null)
|
|
169
|
+
return null; // defensive: not a root
|
|
170
|
+
if (oldMeta.status === 'done')
|
|
171
|
+
return null; // defensive: already parked (rapid double /new)
|
|
172
|
+
const respawn = deps.relaunchRootInPane ?? relaunchRootInPane;
|
|
173
|
+
// Resolve where the new pi will live (pane authoritative; fall back to old
|
|
174
|
+
// meta when paneLocation can't resolve, e.g. outside a live tmux server).
|
|
175
|
+
const loc = paneLocation(pane) ?? {
|
|
176
|
+
session: oldMeta.tmux_session ?? null,
|
|
177
|
+
window: oldMeta.window ?? null,
|
|
178
|
+
};
|
|
179
|
+
const newId = newNodeId();
|
|
180
|
+
const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
181
|
+
// Park-old + mint-new is the single most fragile spot in the runtime, so it is
|
|
182
|
+
// ONE atomic unit: every ROW write below runs inside a sqlite transaction. A
|
|
183
|
+
// failure anywhere — including the respawn DISPATCH — rolls the whole thing
|
|
184
|
+
// back, leaving the old root EXACTLY as it was (no hand-rolled compensation).
|
|
185
|
+
// Only the *detached* respawn (the async pane kill) lands outside the txn — it
|
|
186
|
+
// must, since it kills this caller, and by then COMMIT has made the new state
|
|
187
|
+
// durable. setFocus is a file write, not in the txn; the catch restores it.
|
|
188
|
+
const db = openDb();
|
|
189
|
+
db.exec('BEGIN');
|
|
82
190
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
191
|
+
// 1) Reap descendants (mark done + kill windows, keep edges, no wipe).
|
|
192
|
+
reapDescendants(oldId);
|
|
193
|
+
// 2) Create the fresh root node (new id, empty context dir via
|
|
194
|
+
// ensureNodeDirs) seeded active; `yield` adds the refresh safety net so
|
|
195
|
+
// that if the pane dies before boot the daemon revives it in a new window.
|
|
196
|
+
spawnNode({
|
|
197
|
+
kind: oldMeta.kind,
|
|
85
198
|
mode: 'base',
|
|
86
199
|
lifecycle: 'resident',
|
|
87
|
-
|
|
88
|
-
|
|
200
|
+
cwd: oldMeta.cwd,
|
|
201
|
+
name: oldMeta.kind,
|
|
202
|
+
parent: null,
|
|
203
|
+
spawnedBy: oldId, // audit-only successor link; does NOT touch the spine
|
|
204
|
+
nodeId: newId,
|
|
89
205
|
launch,
|
|
90
|
-
...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
|
|
91
206
|
});
|
|
207
|
+
transition(newId, 'yield'); // active (from spawn) + intent=refresh safety net
|
|
208
|
+
setPresence(newId, { tmux_session: loc.session, window: loc.window });
|
|
209
|
+
// REVIVE-HOME: the relaunched root's durable revive target is the session
|
|
210
|
+
// of the pane it is respawned into (same pane-recycle rule as demote).
|
|
211
|
+
updateNode(newId, { home_session: loc.session ?? nodeSession() });
|
|
212
|
+
clearPid(newId); // no pi yet → daemon 'leave' until boot records the pid
|
|
213
|
+
// 3) Park the old root: reap (done + intent cleared) and detach its window so
|
|
214
|
+
// it never claims the pane, but KEEP pi_session_id (resumable),
|
|
215
|
+
// parent=null, and all edges.
|
|
216
|
+
transition(oldId, 'reap');
|
|
217
|
+
setPresence(oldId, { window: null, tmux_session: null });
|
|
218
|
+
// 4) Focus follows content (file write — restored by the catch on rollback).
|
|
219
|
+
setFocus(newId);
|
|
220
|
+
// 5) Re-exec pi in this pane bound to newId; the dispatch is the LAST thing
|
|
221
|
+
// inside the txn. If it throws the txn rolls back (old root untouched); on
|
|
222
|
+
// success we COMMIT and the async detached kill of this pane lands after.
|
|
223
|
+
respawn(newId, pane);
|
|
224
|
+
db.exec('COMMIT');
|
|
92
225
|
}
|
|
93
|
-
catch {
|
|
94
|
-
|
|
226
|
+
catch (err) {
|
|
227
|
+
// Dispatch failed (or a write threw) — the live pi never died. Roll the whole
|
|
228
|
+
// transaction back so the old root is FULLY restored, then degrade.
|
|
229
|
+
try {
|
|
230
|
+
db.exec('ROLLBACK');
|
|
231
|
+
}
|
|
232
|
+
catch { /* */ }
|
|
233
|
+
// The rolled-back new node's row is gone, but spawnNode already scaffolded its
|
|
234
|
+
// on-disk dir (ensureNodeDirs). With no row, prune never sees it — so remove
|
|
235
|
+
// the orphan dir here, otherwise it is permanent disk litter on this rare path.
|
|
236
|
+
try {
|
|
237
|
+
rmSync(nodeDir(newId), { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
catch { /* */ }
|
|
240
|
+
try {
|
|
241
|
+
setFocus(oldId);
|
|
242
|
+
}
|
|
243
|
+
catch { /* */ } // focus is a file op, outside the txn
|
|
244
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
95
245
|
}
|
|
96
|
-
return {
|
|
246
|
+
return { newNodeId: newId };
|
|
247
|
+
}
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// markCleanExitDone — the clean-exit→done termination guard
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
/** Resolve a cleanly-exiting node to `done`. Returns true iff it transitioned.
|
|
252
|
+
* Guard: only a real quit, and only a node still active|idle with no pending
|
|
253
|
+
* intent — so it never clobbers a node already routed by agent_end to done
|
|
254
|
+
* (push final), refresh (yield), or idle-release. Pure/DB-only (no pi/tmux) so
|
|
255
|
+
* the guard is unit-testable without a live pi. */
|
|
256
|
+
export function markCleanExitDone(nodeId, reason) {
|
|
257
|
+
if (reason !== 'quit')
|
|
258
|
+
return false; // new/reload/resume/fork → no-op
|
|
259
|
+
const meta = getNode(nodeId);
|
|
260
|
+
if (meta === null)
|
|
261
|
+
return false;
|
|
262
|
+
if (meta.status !== 'active' && meta.status !== 'idle')
|
|
263
|
+
return false; // already done/dead/canceled
|
|
264
|
+
if (meta.intent != null)
|
|
265
|
+
return false; // refresh / idle-release in flight
|
|
266
|
+
transition(nodeId, 'finalize');
|
|
267
|
+
return true;
|
|
97
268
|
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
import { type NodeMeta } from '../canvas/index.js';
|
|
2
|
+
import { type RespawnPaneOpts } from './tmux.js';
|
|
3
|
+
/** Pick the `--session` source for a revive. resume=true prefers the absolute
|
|
4
|
+
* session-file path (immune to cwd; pi opens it directly) and keeps the bare
|
|
5
|
+
* session id as the fallback for older nodes booted before pi_session_file was
|
|
6
|
+
* captured. buildPiArgv prefers the path when both are present. resume=false (a
|
|
7
|
+
* refresh-yield) selects neither — the node re-reads its roadmap fresh. Pure so
|
|
8
|
+
* the path-vs-id selection is unit-testable without tmux. */
|
|
9
|
+
export declare function resumeArgs(meta: NodeMeta, resume: boolean): {
|
|
10
|
+
resumeSessionId?: string;
|
|
11
|
+
resumeSessionPath?: string;
|
|
12
|
+
};
|
|
1
13
|
export interface ReviveResult {
|
|
2
14
|
/** The new tmux window id, or null if openNodeWindow failed. */
|
|
3
15
|
window: string | null;
|
|
4
16
|
/** The tmux session the node was placed in. */
|
|
5
17
|
session: string;
|
|
6
|
-
/** True when pi was instructed to resume its saved conversation (`--
|
|
18
|
+
/** True when pi was instructed to resume its saved conversation (`--session <id>`). */
|
|
7
19
|
resumed: boolean;
|
|
8
20
|
}
|
|
9
21
|
/** Open a fresh background tmux window for `nodeId` and update canvas meta.
|
|
@@ -23,4 +35,16 @@ export declare function reviveNode(nodeId: string, opts: {
|
|
|
23
35
|
* `pane` is the target pane id (the yielding node reads it from $TMUX_PANE).
|
|
24
36
|
* Throws on unknown node or when the respawn could not be dispatched, so the
|
|
25
37
|
* caller can fall back to a plain shutdown (daemon revives in a new window). */
|
|
26
|
-
export declare function reviveInPlace(nodeId: string, pane: string): ReviveResult;
|
|
38
|
+
export declare function reviveInPlace(nodeId: string, pane: string, respawn?: (opts: RespawnPaneOpts) => boolean): ReviveResult;
|
|
39
|
+
/** Re-exec a FRESH pi for `nodeId` in EXISTING `pane` (respawn-pane -k), with
|
|
40
|
+
* NO prompt and NO resume — a clean root conversation (goal-capture /
|
|
41
|
+
* context-intro handle the first message + bearings, exactly like bare
|
|
42
|
+
* `crtr`). Unlike reviveInPlace: no buildReviveKickoff prompt, no cycles bump,
|
|
43
|
+
* and it sets CRTR_FRONT_DOOR=1 (REQUIRED — src/core/runtime/CLAUDE.md: any
|
|
44
|
+
* path that boots a pi must guard against a removed/renamed subcommand
|
|
45
|
+
* fork-bombing). Throws if the respawn could not be dispatched.
|
|
46
|
+
*
|
|
47
|
+
* Used by relaunchRoot (reset.ts) for the `/new`-in-a-root relaunch. Kept
|
|
48
|
+
* SEPARATE from reviveInPlace so the refresh-yield path's exact semantics
|
|
49
|
+
* (kickoff + cycle bump) are untouched. */
|
|
50
|
+
export declare function relaunchRootInPane(nodeId: string, pane: string): void;
|
|
@@ -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 { piCommand, respawnPane, nodeSession } from './tmux.js';
|
|
23
|
+
import { reviveIntoPlacement, reconcile, isNodePaneAlive, homeSessionOf } from './placement.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;
|