@crouton-kit/crouter 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +45 -67
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -99
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +129 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +196 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +266 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +178 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +334 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +105 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +205 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +81 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +24 -12
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +26 -12
- package/dist/core/runtime/launch.js +78 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +39 -1
- package/dist/core/runtime/nodes.js +69 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +299 -0
- package/dist/core/runtime/placement.js +688 -0
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +113 -20
- package/dist/core/runtime/tmux.js +221 -39
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +594 -262
- package/dist/pi-extensions/canvas-resume.d.ts +22 -0
- package/dist/pi-extensions/canvas-resume.js +173 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +340 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +2 -2
- package/dist/core/runtime/presence.d.ts +0 -38
- package/dist/core/runtime/presence.js +0 -154
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { type NodeMeta } from '../canvas/index.js';
|
|
2
2
|
export interface PromoteResult {
|
|
3
3
|
meta: NodeMeta;
|
|
4
|
-
/** Orchestration guidance to surface into the node's current context now. */
|
|
5
|
-
guidance: string;
|
|
6
4
|
roadmapWritten: boolean;
|
|
7
5
|
/** Absolute path to the node's roadmap doc (context/roadmap.md). */
|
|
8
6
|
roadmapPath: string;
|
|
9
7
|
/** Absolute path to the node's goal doc (context/initial-prompt.md). */
|
|
10
8
|
goalPath: string;
|
|
9
|
+
/** Absolute path to the node-local memory index (context/memory/MEMORY.md). */
|
|
10
|
+
memoryPath: string;
|
|
11
|
+
/** Absolute path to the user-global memory index (<crtrHome>/memory/MEMORY.md). */
|
|
12
|
+
userMemoryPath: string;
|
|
13
|
+
/** Absolute path to the project memory index (<crtrHome>/projects/<key>/memory/MEMORY.md). */
|
|
14
|
+
projectMemoryPath: string;
|
|
11
15
|
}
|
|
12
|
-
/** Promote a node to
|
|
13
|
-
* (e.g. a `general` worker becoming a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* is
|
|
16
|
+
/** Promote a node to an orchestrator (mode→orchestrator), optionally
|
|
17
|
+
* specializing its kind (e.g. a `general` worker becoming a
|
|
18
|
+
* `developer.orchestrator`) and optionally also making it resident. Idempotent:
|
|
19
|
+
* re-promoting just rewrites the spec. Seeds a roadmap SCAFFOLD if absent (a
|
|
20
|
+
* boss with no map is a failure mode) — no goal is forced here; authoring the
|
|
21
|
+
* goal + roadmap is the node's next act. The transition guidance is injected
|
|
22
|
+
* centrally by the persona injector at the next turn boundary, not returned. */
|
|
17
23
|
export declare function promote(nodeId: string, opts?: {
|
|
18
24
|
kind?: string;
|
|
25
|
+
resident?: boolean;
|
|
19
26
|
}): PromoteResult;
|
|
20
27
|
export interface YieldResult {
|
|
21
28
|
meta: NodeMeta;
|
|
@@ -1,71 +1,36 @@
|
|
|
1
|
-
// Promotion —
|
|
1
|
+
// Promotion — the worker→orchestrator polymorph (mode→orchestrator).
|
|
2
2
|
//
|
|
3
3
|
// Two stages (the pi-mode-switch pattern):
|
|
4
|
-
// 1. Promotion →
|
|
5
|
-
// node's mode
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// 1. Promotion → mode flips to orchestrator (mid-turn). This call flips the
|
|
5
|
+
// node's mode and (optionally) its KIND, REWRITES its launch spec to that
|
|
6
|
+
// kind's orchestrator persona (so the next revive comes back as that
|
|
7
|
+
// orchestrator), and seeds a roadmap scaffold + the three memory stores.
|
|
8
|
+
// The transition guidance the node needs is injected CENTRALLY by the
|
|
9
|
+
// persona injector (runtime/persona.ts) at the turn boundary — promote()
|
|
10
|
+
// itself no longer returns or hand-emits guidance.
|
|
10
11
|
// 2. Refresh → persona swap (permanent). On the next fresh revive the node
|
|
11
12
|
// starts with the orchestrator system prompt baked in (because the launch
|
|
12
|
-
// spec now says orchestrator). The guidance
|
|
13
|
+
// spec now says orchestrator). The injected guidance bridges until then.
|
|
14
|
+
//
|
|
15
|
+
// Mode and lifecycle are ORTHOGONAL: promotion flips mode only. Lifecycle stays
|
|
16
|
+
// whatever it was (a promoted child is terminal/orchestrator — still reports up
|
|
17
|
+
// + reaps) unless the caller passes `resident:true` to also make it resident.
|
|
13
18
|
//
|
|
14
19
|
// Trigger is persistence-need (deliberate, or a refresh-yield with open work),
|
|
15
20
|
// never the mere act of spawning a child.
|
|
16
|
-
import { getNode, updateNode
|
|
21
|
+
import { getNode, updateNode } from '../canvas/index.js';
|
|
22
|
+
import { transition } from './lifecycle.js';
|
|
17
23
|
import { buildLaunchSpec } from './launch.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { readText } from '../fs-utils.js';
|
|
21
|
-
import { parseFrontmatter } from '../frontmatter.js';
|
|
22
|
-
import { hasRoadmap, seedRoadmap, readRoadmap, roadmapPath } from './roadmap.js';
|
|
24
|
+
import { hasRoadmap, seedRoadmap, roadmapPath } from './roadmap.js';
|
|
25
|
+
import { seedMemory, memoryPath, seedUserMemory, userMemoryPath, seedProjectMemory, projectMemoryPath, } from './memory.js';
|
|
23
26
|
import { readGoal, goalPath } from './kickoff.js';
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/** Build the mid-turn guidance dump, specialized to the node's (possibly
|
|
36
|
-
* just-chosen) kind: the shared kernel + that kind's roadmap-shaping skill
|
|
37
|
-
* (auto-loaded now, before the persona swap bakes in on revive) + the roadmap
|
|
38
|
-
* scaffold the node must author. No goal is assumed — writing it is step one. */
|
|
39
|
-
function orchestrationGuidance(nodeId, kind) {
|
|
40
|
-
const kernel = loadKernel();
|
|
41
|
-
const orch = loadPersona(kind, 'orchestrator');
|
|
42
|
-
const roadmapSkill = typeof orch?.frontmatter?.['roadmapSkill'] === 'string'
|
|
43
|
-
? orch.frontmatter['roadmapSkill']
|
|
44
|
-
: undefined;
|
|
45
|
-
const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
|
|
46
|
-
const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
|
|
47
|
-
const rmPath = roadmapPath(nodeId);
|
|
48
|
-
const goal = readGoal(nodeId);
|
|
49
|
-
const parts = [
|
|
50
|
-
`You are now a RESIDENT ${kind.toUpperCase()} ORCHESTRATOR. Your scarce resource is your own context window.`,
|
|
51
|
-
'Your job is to manage context and delegate — not to do the goal yourself.',
|
|
52
|
-
'',
|
|
53
|
-
kernel,
|
|
54
|
-
];
|
|
55
|
-
if (goal !== null && goal.trim() !== '') {
|
|
56
|
-
parts.push('', `--- Your goal (${goalPath(nodeId)}) ---`, '', goal.trim());
|
|
57
|
-
}
|
|
58
|
-
if (skillBody) {
|
|
59
|
-
parts.push('', `--- How to shape a ${kind} roadmap (skill: ${roadmapSkill}) ---`, '', skillBody);
|
|
60
|
-
}
|
|
61
|
-
parts.push('', `Your roadmap scaffold (\`${rmPath}\`) — author it now: state the goal, exit criteria, and the phase skeleton, using the approach above. Current contents:`, '', roadmap, '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
|
|
62
|
-
return parts.join('\n');
|
|
63
|
-
}
|
|
64
|
-
/** Promote a node to resident orchestrator, optionally specializing its kind
|
|
65
|
-
* (e.g. a `general` worker becoming a `developer.orchestrator`). Idempotent:
|
|
66
|
-
* re-promoting just rewrites the spec + returns fresh guidance. Seeds a
|
|
67
|
-
* roadmap SCAFFOLD if absent (a boss with no map is a failure mode) — no goal
|
|
68
|
-
* is forced here; authoring the goal + roadmap is the node's next act. */
|
|
27
|
+
/** Promote a node to an orchestrator (mode→orchestrator), optionally
|
|
28
|
+
* specializing its kind (e.g. a `general` worker becoming a
|
|
29
|
+
* `developer.orchestrator`) and optionally also making it resident. Idempotent:
|
|
30
|
+
* re-promoting just rewrites the spec. Seeds a roadmap SCAFFOLD if absent (a
|
|
31
|
+
* boss with no map is a failure mode) — no goal is forced here; authoring the
|
|
32
|
+
* goal + roadmap is the node's next act. The transition guidance is injected
|
|
33
|
+
* centrally by the persona injector at the next turn boundary, not returned. */
|
|
69
34
|
export function promote(nodeId, opts = {}) {
|
|
70
35
|
const node = getNode(nodeId);
|
|
71
36
|
if (node === null)
|
|
@@ -76,7 +41,13 @@ export function promote(nodeId, opts = {}) {
|
|
|
76
41
|
// *next* revive comes back orchestrating in that kind (polymorph stage 2).
|
|
77
42
|
// nodeEnv reads meta.{kind,mode}, so CRTR_KIND/CRTR_MODE flip immediately for
|
|
78
43
|
// the live process's children too.
|
|
79
|
-
|
|
44
|
+
// Bake the node's post-promote lifecycle + spine into the rebuilt prompt:
|
|
45
|
+
// lifecycle becomes resident only when the caller asked (else it keeps its
|
|
46
|
+
// current value); spine is fixed by parent-ness (immutable).
|
|
47
|
+
const { launch } = buildLaunchSpec(targetKind, 'orchestrator', {
|
|
48
|
+
lifecycle: opts.resident === true ? 'resident' : node.lifecycle,
|
|
49
|
+
hasManager: node.parent !== null,
|
|
50
|
+
});
|
|
80
51
|
// Seed a barebones roadmap scaffold if absent so the file exists for a
|
|
81
52
|
// refresh. Pre-fill its Goal from the node's goal doc when present (set at
|
|
82
53
|
// spawn, or captured from the first user message); the node fleshes out the
|
|
@@ -87,13 +58,30 @@ export function promote(nodeId, opts = {}) {
|
|
|
87
58
|
seedRoadmap(nodeId, goal !== null && goal.trim() !== '' ? { goal: goal.trim() } : {});
|
|
88
59
|
roadmapWritten = true;
|
|
89
60
|
}
|
|
90
|
-
|
|
61
|
+
// Seed all three scoped memory stores alongside the roadmap — user-global,
|
|
62
|
+
// project (keyed off this node's cwd), and node-local. Each is a durable,
|
|
63
|
+
// refresh-surviving artifact; each guarded so a re-seed never clobbers an
|
|
64
|
+
// evolved memory.
|
|
65
|
+
seedUserMemory();
|
|
66
|
+
seedProjectMemory(node.cwd);
|
|
67
|
+
seedMemory(nodeId);
|
|
68
|
+
// Flip mode→orchestrator + kind + launch spec. Lifecycle is independent:
|
|
69
|
+
// only set resident when the caller asked for it (the common self-promotion
|
|
70
|
+
// stays terminal/orchestrator — it still reports up + reaps).
|
|
71
|
+
const meta = updateNode(nodeId, {
|
|
72
|
+
kind: targetKind,
|
|
73
|
+
mode: 'orchestrator',
|
|
74
|
+
launch,
|
|
75
|
+
...(opts.resident === true ? { lifecycle: 'resident' } : {}),
|
|
76
|
+
});
|
|
91
77
|
return {
|
|
92
78
|
meta,
|
|
93
|
-
guidance: orchestrationGuidance(nodeId, targetKind),
|
|
94
79
|
roadmapWritten,
|
|
95
80
|
roadmapPath: roadmapPath(nodeId),
|
|
96
81
|
goalPath: goalPath(nodeId),
|
|
82
|
+
memoryPath: memoryPath(nodeId),
|
|
83
|
+
userMemoryPath: userMemoryPath(),
|
|
84
|
+
projectMemoryPath: projectMemoryPath(node.cwd),
|
|
97
85
|
};
|
|
98
86
|
}
|
|
99
87
|
/** Request a refresh-yield: discard in-memory context and revive fresh against
|
|
@@ -107,14 +95,16 @@ export function requestYield(nodeId, opts = {}) {
|
|
|
107
95
|
if (node === null)
|
|
108
96
|
throw new Error(`unknown node: ${nodeId}`);
|
|
109
97
|
let promoted = false;
|
|
110
|
-
if (node.
|
|
111
|
-
//
|
|
112
|
-
// (
|
|
98
|
+
if (node.mode !== 'orchestrator') {
|
|
99
|
+
// A yield needs a ROADMAP to refresh against — i.e. orchestrator mode, not
|
|
100
|
+
// resident lifecycle. Ensure orchestrator (which seeds the roadmap + memory)
|
|
101
|
+
// WITHOUT forcing resident: a terminal/orchestrator yields fine, since the
|
|
102
|
+
// daemon's refresh-revive keys on intent='refresh', not lifecycle.
|
|
113
103
|
promote(nodeId, opts.kind !== undefined ? { kind: opts.kind } : {});
|
|
114
104
|
promoted = true;
|
|
115
105
|
}
|
|
116
106
|
// Mark the intent; the stophook enacts the shutdown, the daemon the revive.
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
transition(nodeId, 'yield');
|
|
108
|
+
const meta = getNode(nodeId);
|
|
119
109
|
return { meta, promoted, willRefresh: true };
|
|
120
110
|
}
|
|
@@ -1,13 +1,56 @@
|
|
|
1
|
+
/** Reap the descendant sub-DAG of `rootId`: mark each **done** (the user moved
|
|
2
|
+
* on — a clean teardown, NOT a fault) + clear intent FIRST, then kill its
|
|
3
|
+
* window (closes the daemon revive race). Edges are LEFT INTACT — descendants
|
|
4
|
+
* keep parent=rootId. No wipe. Returns the reaped ids.
|
|
5
|
+
*
|
|
6
|
+
* Why `done`, and why marking is STILL explicit: a `closeWindow`/`respawn-pane
|
|
7
|
+
* -k` kill is abrupt and fires NO clean `session_shutdown`, so the general
|
|
8
|
+
* quit→done rule does NOT auto-resolve a force-killed descendant — we mark it
|
|
9
|
+
* `done` here. Shared by relaunchRoot (option C) and resetRoot's in-place
|
|
10
|
+
* fallback, so both leave their descendants `done`. */
|
|
11
|
+
export declare function reapDescendants(rootId: string): string[];
|
|
1
12
|
export interface ResetRootResult {
|
|
2
|
-
/** Descendant node ids torn down (window killed + marked
|
|
13
|
+
/** Descendant node ids torn down (window killed + marked done). */
|
|
3
14
|
reaped: string[];
|
|
4
15
|
/** Direct subscriptions dropped off the root. */
|
|
5
16
|
detached: string[];
|
|
6
17
|
/** True when the node was a root and a full reset ran. */
|
|
7
18
|
reset: boolean;
|
|
8
19
|
}
|
|
9
|
-
/** Reset a root node to a pristine, empty graph (the `/new` semantics
|
|
20
|
+
/** Reset a root node to a pristine, empty graph (the legacy `/new` semantics —
|
|
21
|
+
* now used as the no-pane fallback and the non-root session-id refresh).
|
|
10
22
|
*
|
|
11
23
|
* For a non-root (spawned child), a `/new` is not a graph reset — we only
|
|
12
|
-
* refresh its session id so a later `--
|
|
13
|
-
export declare function resetRoot(nodeId: string, newSessionId?: string): ResetRootResult;
|
|
24
|
+
* refresh its session id so a later `--session <id>` wakes the right conversation. */
|
|
25
|
+
export declare function resetRoot(nodeId: string, newSessionId?: string, newSessionFile?: string | null): ResetRootResult;
|
|
26
|
+
/** Injectable respawn seam — tests pass a double since tmux isn't available. */
|
|
27
|
+
export interface RelaunchDeps {
|
|
28
|
+
relaunchRootInPane?: (nodeId: string, pane: string) => void;
|
|
29
|
+
}
|
|
30
|
+
export type HandleNewSessionPath = 'relaunch' | 'reset-root' | 'reset-child' | 'noop';
|
|
31
|
+
export interface HandleNewSessionResult {
|
|
32
|
+
path: HandleNewSessionPath;
|
|
33
|
+
newNodeId?: string;
|
|
34
|
+
}
|
|
35
|
+
/** The single entry the stophook calls on a detected `/new` (session id change).
|
|
36
|
+
* Policy lives here so the stophook stays thin and this stays unit-testable:
|
|
37
|
+
* - non-root child → resetRoot(nodeId, newSessionId) (session-id refresh only)
|
|
38
|
+
* - root + pane present → relaunchRoot(nodeId, pane) (option C)
|
|
39
|
+
* - root + no pane (no tmux) → resetRoot(nodeId, newSessionId) (in-place fallback)
|
|
40
|
+
* On a respawn-dispatch failure the live pi never died, so we degrade to the
|
|
41
|
+
* legacy in-place reset. */
|
|
42
|
+
export declare function handleNewSession(nodeId: string, newSessionId: string, pane: string | undefined, deps?: RelaunchDeps, newSessionFile?: string | null): HandleNewSessionResult;
|
|
43
|
+
/** Park the old root + create+launch a fresh root in `pane` (option C). All DB
|
|
44
|
+
* writes are synchronous and happen BEFORE the respawn (the respawn kills the
|
|
45
|
+
* caller). Returns the new node id, or null on a defensive guard (not a root /
|
|
46
|
+
* already parked). Throws only if the respawn dispatch fails — and self-rolls-
|
|
47
|
+
* back its writes first so the caller can degrade to resetRoot. */
|
|
48
|
+
export declare function relaunchRoot(oldId: string, pane: string, deps?: RelaunchDeps): {
|
|
49
|
+
newNodeId: string;
|
|
50
|
+
} | null;
|
|
51
|
+
/** Resolve a cleanly-exiting node to `done`. Returns true iff it transitioned.
|
|
52
|
+
* Guard: only a real quit, and only a node still active|idle with no pending
|
|
53
|
+
* intent — so it never clobbers a node already routed by agent_end to done
|
|
54
|
+
* (push final), refresh (yield), or idle-release. Pure/DB-only (no pi/tmux) so
|
|
55
|
+
* the guard is unit-testable without a live pi. */
|
|
56
|
+
export declare function markCleanExitDone(nodeId: string, reason: unknown): boolean;
|
|
@@ -1,67 +1,89 @@
|
|
|
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, setFocusOccupant, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, nodeDir, openDb, } from '../canvas/index.js';
|
|
23
|
+
import { transition } from './lifecycle.js';
|
|
24
|
+
import { paneLocation, tearDownNode, focusOf } from './placement.js';
|
|
16
25
|
import { buildLaunchSpec } from './launch.js';
|
|
17
26
|
import { roadmapPath } from './roadmap.js';
|
|
18
|
-
|
|
27
|
+
import { spawnNode, newNodeId, nodeSession } from './nodes.js';
|
|
28
|
+
import { relaunchRootInPane } from './revive.js';
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// reapDescendants — tear down a root's descendant sub-DAG (shared helper)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
/** Reap the descendant sub-DAG of `rootId`: mark each **done** (the user moved
|
|
33
|
+
* on — a clean teardown, NOT a fault) + clear intent FIRST, then kill its
|
|
34
|
+
* window (closes the daemon revive race). Edges are LEFT INTACT — descendants
|
|
35
|
+
* keep parent=rootId. No wipe. Returns the reaped ids.
|
|
36
|
+
*
|
|
37
|
+
* Why `done`, and why marking is STILL explicit: a `closeWindow`/`respawn-pane
|
|
38
|
+
* -k` kill is abrupt and fires NO clean `session_shutdown`, so the general
|
|
39
|
+
* quit→done rule does NOT auto-resolve a force-killed descendant — we mark it
|
|
40
|
+
* `done` here. Shared by relaunchRoot (option C) and resetRoot's in-place
|
|
41
|
+
* fallback, so both leave their descendants `done`. */
|
|
42
|
+
export function reapDescendants(rootId) {
|
|
43
|
+
const reaped = [];
|
|
44
|
+
for (const id of view(rootId)) {
|
|
45
|
+
try {
|
|
46
|
+
// Reap BEFORE tearing down the placement (the crash-safety invariant the
|
|
47
|
+
// `reap` event encodes): a non-supervised status + cleared intent first, so
|
|
48
|
+
// the daemon can't revive a descendant mid-teardown. tearDownNode then
|
|
49
|
+
// closes any focus row it held, kills its pane (pane-keyed), and nulls its
|
|
50
|
+
// LOCATION.
|
|
51
|
+
transition(id, 'reap');
|
|
52
|
+
tearDownNode(id);
|
|
53
|
+
reaped.push(id);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* one bad node never aborts the reap */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return reaped;
|
|
60
|
+
}
|
|
61
|
+
/** Reset a root node to a pristine, empty graph (the legacy `/new` semantics —
|
|
62
|
+
* now used as the no-pane fallback and the non-root session-id refresh).
|
|
19
63
|
*
|
|
20
64
|
* 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) {
|
|
65
|
+
* refresh its session id so a later `--session <id>` wakes the right conversation. */
|
|
66
|
+
export function resetRoot(nodeId, newSessionId, newSessionFile) {
|
|
23
67
|
const meta = getNode(nodeId);
|
|
24
68
|
if (meta === null)
|
|
25
69
|
return { reaped: [], detached: [], reset: false };
|
|
26
70
|
// Only roots own a graph in the "ran crtr again" sense.
|
|
27
71
|
if (meta.parent != null) {
|
|
28
72
|
if (newSessionId !== undefined) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
73
|
+
updateNode(nodeId, {
|
|
74
|
+
pi_session_id: newSessionId,
|
|
75
|
+
...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
|
|
76
|
+
});
|
|
33
77
|
}
|
|
34
78
|
return { reaped: [], detached: [], reset: false };
|
|
35
79
|
}
|
|
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
|
-
}
|
|
80
|
+
// 1) Reap the descendant sub-DAG (mark done + kill windows; shared helper).
|
|
81
|
+
const reaped = reapDescendants(nodeId);
|
|
55
82
|
// 2) Detach the root's own subscriptions so its view is empty.
|
|
56
83
|
const detached = [];
|
|
57
84
|
for (const sub of subscriptionsOf(nodeId)) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
detached.push(sub.node_id);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
/* */
|
|
64
|
-
}
|
|
85
|
+
unsubscribe(nodeId, sub.node_id);
|
|
86
|
+
detached.push(sub.node_id);
|
|
65
87
|
}
|
|
66
88
|
// 3) Wipe the root's working state (reports / inbox / roadmap).
|
|
67
89
|
for (const p of [
|
|
@@ -78,20 +100,169 @@ export function resetRoot(nodeId, newSessionId) {
|
|
|
78
100
|
/* */
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
|
-
// 4) Re-point the root at a fresh base persona + the new pi session id.
|
|
103
|
+
// 4) Re-point the root at a fresh base persona + the new pi session id. A
|
|
104
|
+
// root is resident by definition (this only runs on roots — see the early
|
|
105
|
+
// return above), so resetting to base/resident is the model, not a bypass.
|
|
106
|
+
// Re-seed persona_ack to the fresh persona so the pristine `/new`
|
|
107
|
+
// conversation never gets a spurious mode/lifecycle transition steer (the
|
|
108
|
+
// persona injector compares against this ack).
|
|
109
|
+
const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
110
|
+
updateNode(nodeId, {
|
|
111
|
+
mode: 'base',
|
|
112
|
+
lifecycle: 'resident',
|
|
113
|
+
persona_ack: { mode: 'base', lifecycle: 'resident' },
|
|
114
|
+
launch,
|
|
115
|
+
...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
|
|
116
|
+
...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
|
|
117
|
+
});
|
|
118
|
+
transition(nodeId, 'revive');
|
|
119
|
+
return { reaped, detached, reset: true };
|
|
120
|
+
}
|
|
121
|
+
/** The single entry the stophook calls on a detected `/new` (session id change).
|
|
122
|
+
* Policy lives here so the stophook stays thin and this stays unit-testable:
|
|
123
|
+
* - non-root child → resetRoot(nodeId, newSessionId) (session-id refresh only)
|
|
124
|
+
* - root + pane present → relaunchRoot(nodeId, pane) (option C)
|
|
125
|
+
* - root + no pane (no tmux) → resetRoot(nodeId, newSessionId) (in-place fallback)
|
|
126
|
+
* On a respawn-dispatch failure the live pi never died, so we degrade to the
|
|
127
|
+
* legacy in-place reset. */
|
|
128
|
+
export function handleNewSession(nodeId, newSessionId, pane, deps = {}, newSessionFile) {
|
|
129
|
+
const meta = getNode(nodeId);
|
|
130
|
+
if (meta === null)
|
|
131
|
+
return { path: 'noop' };
|
|
132
|
+
// Non-root child: a `/new` only refreshes its session id (unchanged).
|
|
133
|
+
if (meta.parent != null) {
|
|
134
|
+
resetRoot(nodeId, newSessionId, newSessionFile);
|
|
135
|
+
return { path: 'reset-child' };
|
|
136
|
+
}
|
|
137
|
+
// Root with no pane (not inside tmux): in-place reset fallback. Option C needs
|
|
138
|
+
// a pane to respawn into; resetRoot needs the new session id (available here
|
|
139
|
+
// because the trigger is session_start).
|
|
140
|
+
if (pane === undefined || pane.trim() === '') {
|
|
141
|
+
resetRoot(nodeId, newSessionId, newSessionFile);
|
|
142
|
+
return { path: 'reset-root' };
|
|
143
|
+
}
|
|
144
|
+
// Root with a pane: option C relaunch. relaunchRoot self-rolls-back its DB
|
|
145
|
+
// writes on a respawn-dispatch failure and rethrows; we then degrade to the
|
|
146
|
+
// legacy in-place reset (the live pi is still alive, never killed).
|
|
147
|
+
try {
|
|
148
|
+
const result = relaunchRoot(nodeId, pane, deps);
|
|
149
|
+
if (result === null)
|
|
150
|
+
return { path: 'noop' }; // defensive guard hit (e.g. rapid double /new)
|
|
151
|
+
return { path: 'relaunch', newNodeId: result.newNodeId };
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
resetRoot(nodeId, newSessionId, newSessionFile);
|
|
155
|
+
return { path: 'reset-root' };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Park the old root + create+launch a fresh root in `pane` (option C). All DB
|
|
159
|
+
* writes are synchronous and happen BEFORE the respawn (the respawn kills the
|
|
160
|
+
* caller). Returns the new node id, or null on a defensive guard (not a root /
|
|
161
|
+
* already parked). Throws only if the respawn dispatch fails — and self-rolls-
|
|
162
|
+
* back its writes first so the caller can degrade to resetRoot. */
|
|
163
|
+
export function relaunchRoot(oldId, pane, deps = {}) {
|
|
164
|
+
const oldMeta = getNode(oldId);
|
|
165
|
+
if (oldMeta === null || oldMeta.parent != null)
|
|
166
|
+
return null; // defensive: not a root
|
|
167
|
+
if (oldMeta.status === 'done')
|
|
168
|
+
return null; // defensive: already parked (rapid double /new)
|
|
169
|
+
const respawn = deps.relaunchRootInPane ?? relaunchRootInPane;
|
|
170
|
+
// Resolve where the new pi will live (pane authoritative; fall back to old
|
|
171
|
+
// meta when paneLocation can't resolve, e.g. outside a live tmux server).
|
|
172
|
+
const loc = paneLocation(pane) ?? {
|
|
173
|
+
session: oldMeta.tmux_session ?? null,
|
|
174
|
+
window: oldMeta.window ?? null,
|
|
175
|
+
};
|
|
176
|
+
const newId = newNodeId();
|
|
177
|
+
const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
178
|
+
// Park-old + mint-new is the single most fragile spot in the runtime, so it is
|
|
179
|
+
// ONE atomic unit: every ROW write below runs inside a sqlite transaction. A
|
|
180
|
+
// failure anywhere — including the respawn DISPATCH — rolls the whole thing
|
|
181
|
+
// back, leaving the old root EXACTLY as it was (no hand-rolled compensation).
|
|
182
|
+
// Only the *detached* respawn (the async pane kill) lands outside the txn — it
|
|
183
|
+
// must, since it kills this caller, and by then COMMIT has made the new state
|
|
184
|
+
// durable. The focus repoint (step 4) is INSIDE the txn, so a ROLLBACK undoes
|
|
185
|
+
// it automatically — there is no file to restore.
|
|
186
|
+
const db = openDb();
|
|
187
|
+
db.exec('BEGIN');
|
|
82
188
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
189
|
+
// 1) Reap descendants (mark done + kill windows, keep edges, no wipe).
|
|
190
|
+
reapDescendants(oldId);
|
|
191
|
+
// 2) Create the fresh root node (new id, empty context dir via
|
|
192
|
+
// ensureNodeDirs) seeded active; `yield` adds the refresh safety net so
|
|
193
|
+
// that if the pane dies before boot the daemon revives it in a new window.
|
|
194
|
+
spawnNode({
|
|
195
|
+
kind: oldMeta.kind,
|
|
85
196
|
mode: 'base',
|
|
86
197
|
lifecycle: 'resident',
|
|
87
|
-
|
|
88
|
-
|
|
198
|
+
cwd: oldMeta.cwd,
|
|
199
|
+
name: oldMeta.kind,
|
|
200
|
+
parent: null,
|
|
201
|
+
spawnedBy: oldId, // audit-only successor link; does NOT touch the spine
|
|
202
|
+
nodeId: newId,
|
|
89
203
|
launch,
|
|
90
|
-
...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
|
|
91
204
|
});
|
|
205
|
+
transition(newId, 'yield'); // active (from spawn) + intent=refresh safety net
|
|
206
|
+
setPresence(newId, { tmux_session: loc.session, window: loc.window });
|
|
207
|
+
// REVIVE-HOME: the relaunched root's durable revive target is the session
|
|
208
|
+
// of the pane it is respawned into (same pane-recycle rule as demote).
|
|
209
|
+
updateNode(newId, { home_session: loc.session ?? nodeSession() });
|
|
210
|
+
clearPid(newId); // no pi yet → daemon 'leave' until boot records the pid
|
|
211
|
+
// 3) Park the old root: reap (done + intent cleared) and detach its window so
|
|
212
|
+
// it never claims the pane, but KEEP pi_session_id (resumable),
|
|
213
|
+
// parent=null, and all edges.
|
|
214
|
+
transition(oldId, 'reap');
|
|
215
|
+
setPresence(oldId, { window: null, tmux_session: null });
|
|
216
|
+
// 4) Focus follows content: repoint the old root's focus row to the new root
|
|
217
|
+
// (same pane — respawn-pane -k below keeps the %id). Inside the txn → the
|
|
218
|
+
// ROLLBACK path restores the old occupant automatically (no file to restore).
|
|
219
|
+
const oldFocus = focusOf(oldId);
|
|
220
|
+
if (oldFocus !== null)
|
|
221
|
+
setFocusOccupant(oldFocus.focus_id, newId);
|
|
222
|
+
// 5) Re-exec pi in this pane bound to newId; the dispatch is the LAST thing
|
|
223
|
+
// inside the txn. If it throws the txn rolls back (old root untouched); on
|
|
224
|
+
// success we COMMIT and the async detached kill of this pane lands after.
|
|
225
|
+
respawn(newId, pane);
|
|
226
|
+
db.exec('COMMIT');
|
|
92
227
|
}
|
|
93
|
-
catch {
|
|
94
|
-
|
|
228
|
+
catch (err) {
|
|
229
|
+
// Dispatch failed (or a write threw) — the live pi never died. Roll the whole
|
|
230
|
+
// transaction back so the old root is FULLY restored, then degrade.
|
|
231
|
+
try {
|
|
232
|
+
db.exec('ROLLBACK');
|
|
233
|
+
}
|
|
234
|
+
catch { /* */ }
|
|
235
|
+
// The rolled-back new node's row is gone, but spawnNode already scaffolded its
|
|
236
|
+
// on-disk dir (ensureNodeDirs). With no row, prune never sees it — so remove
|
|
237
|
+
// the orphan dir here, otherwise it is permanent disk litter on this rare path.
|
|
238
|
+
try {
|
|
239
|
+
rmSync(nodeDir(newId), { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
catch { /* */ }
|
|
242
|
+
// The focus repoint was inside the txn; ROLLBACK already restored the old
|
|
243
|
+
// occupant — nothing to undo here.
|
|
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 './placement.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;
|